From 3454a55636a412097a3be2fdaec9432bd66878de Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:46:15 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .gitattributes | 1 + .github/dependabot.yml | 17 + .github/labeler.yml | 64 + .github/workflows/cffconvert.yml | 23 + .github/workflows/ci.yml | 160 + .github/workflows/labeler.yml | 15 + .github/workflows/release.yml | 112 + .github/workflows/retest-pr.yml | 71 + .gitignore | 71 + .pre-commit-config.yaml | 28 + AUTHORS.md | 334 + CITATION.cff | 47 + CONTRIBUTING.md | 304 + ChangeLog | 1586 ++++ LICENSE.txt | 202 + MANIFEST.in | 14 + Makefile | 54 + README.md | 50 + README.wehub.md | 7 + RELEASE-Github.md | 46 + RELEASE-HOWTO.txt | 85 + SECURITY.md | 123 + nltk/VERSION | 1 + nltk/__init__.py | 207 + nltk/app/__init__.py | 47 + nltk/app/chartparser_app.py | 2570 ++++++ nltk/app/chunkparser_app.py | 1500 ++++ nltk/app/collocations_app.py | 438 + nltk/app/concordance_app.py | 709 ++ nltk/app/nemo_app.py | 163 + nltk/app/rdparser_app.py | 1052 +++ nltk/app/srparser_app.py | 937 ++ nltk/app/wordfreq_app.py | 36 + nltk/app/wordnet_app.py | 1035 +++ nltk/book.py | 213 + nltk/ccg/__init__.py | 34 + nltk/ccg/api.py | 368 + nltk/ccg/chart.py | 496 ++ nltk/ccg/combinator.py | 340 + nltk/ccg/lexicon.py | 348 + nltk/ccg/logic.py | 151 + nltk/chat/__init__.py | 48 + nltk/chat/eliza.py | 337 + nltk/chat/iesha.py | 160 + nltk/chat/rude.py | 125 + nltk/chat/suntsu.py | 140 + nltk/chat/util.py | 124 + nltk/chat/zen.py | 329 + nltk/chunk/__init__.py | 205 + nltk/chunk/api.py | 56 + nltk/chunk/named_entity.py | 406 + nltk/chunk/regexp.py | 1483 +++ nltk/chunk/util.py | 642 ++ nltk/classify/__init__.py | 101 + nltk/classify/api.py | 195 + nltk/classify/decisiontree.py | 349 + nltk/classify/maxent.py | 1628 ++++ nltk/classify/megam.py | 184 + nltk/classify/naivebayes.py | 260 + nltk/classify/positivenaivebayes.py | 180 + nltk/classify/rte_classify.py | 183 + nltk/classify/scikitlearn.py | 143 + nltk/classify/senna.py | 189 + nltk/classify/svm.py | 17 + nltk/classify/tadm.py | 122 + nltk/classify/textcat.py | 268 + nltk/classify/util.py | 347 + nltk/classify/weka.py | 383 + nltk/cli.py | 55 + nltk/cluster/__init__.py | 92 + nltk/cluster/api.py | 74 + nltk/cluster/em.py | 219 + nltk/cluster/gaac.py | 170 + nltk/cluster/kmeans.py | 230 + nltk/cluster/util.py | 300 + nltk/collections.py | 657 ++ nltk/collocations.py | 421 + nltk/compat.py | 38 + nltk/corpus/__init__.py | 551 ++ nltk/corpus/europarl_raw.py | 56 + nltk/corpus/reader/__init__.py | 186 + nltk/corpus/reader/aligned.py | 154 + nltk/corpus/reader/api.py | 529 ++ nltk/corpus/reader/bcp47.py | 218 + nltk/corpus/reader/bnc.py | 268 + nltk/corpus/reader/bracket_parse.py | 274 + nltk/corpus/reader/categorized_sents.py | 168 + nltk/corpus/reader/chasen.py | 154 + nltk/corpus/reader/childes.py | 649 ++ nltk/corpus/reader/chunked.py | 273 + nltk/corpus/reader/cmudict.py | 93 + nltk/corpus/reader/comparative_sents.py | 309 + nltk/corpus/reader/conll.py | 608 ++ nltk/corpus/reader/crubadan.py | 106 + nltk/corpus/reader/dependency.py | 115 + nltk/corpus/reader/framenet.py | 3469 ++++++++ nltk/corpus/reader/ieer.py | 119 + nltk/corpus/reader/indian.py | 93 + nltk/corpus/reader/ipipan.py | 354 + nltk/corpus/reader/knbc.py | 186 + nltk/corpus/reader/lin.py | 183 + nltk/corpus/reader/markdown.py | 353 + nltk/corpus/reader/mte.py | 398 + nltk/corpus/reader/nkjp.py | 513 ++ nltk/corpus/reader/nombank.py | 480 + nltk/corpus/reader/nps_chat.py | 90 + nltk/corpus/reader/opinion_lexicon.py | 125 + nltk/corpus/reader/panlex_lite.py | 174 + nltk/corpus/reader/panlex_swadesh.py | 95 + nltk/corpus/reader/pl196x.py | 373 + nltk/corpus/reader/plaintext.py | 237 + nltk/corpus/reader/ppattach.py | 95 + nltk/corpus/reader/propbank.py | 524 ++ nltk/corpus/reader/pros_cons.py | 133 + nltk/corpus/reader/reviews.py | 337 + nltk/corpus/reader/rte.py | 146 + nltk/corpus/reader/semcor.py | 299 + nltk/corpus/reader/senseval.py | 205 + nltk/corpus/reader/sentiwordnet.py | 136 + nltk/corpus/reader/sinica_treebank.py | 75 + nltk/corpus/reader/string_category.py | 56 + nltk/corpus/reader/switchboard.py | 125 + nltk/corpus/reader/tagged.py | 354 + nltk/corpus/reader/timit.py | 510 ++ nltk/corpus/reader/toolbox.py | 76 + nltk/corpus/reader/twitter.py | 136 + nltk/corpus/reader/udhr.py | 74 + nltk/corpus/reader/util.py | 812 ++ nltk/corpus/reader/verbnet.py | 652 ++ nltk/corpus/reader/wordlist.py | 182 + nltk/corpus/reader/wordnet.py | 2567 ++++++ nltk/corpus/reader/xmldocs.py | 417 + nltk/corpus/reader/ycoe.py | 256 + nltk/corpus/util.py | 151 + nltk/data.py | 1841 ++++ nltk/decorators.py | 251 + nltk/downloader.py | 3031 +++++++ nltk/draw/__init__.py | 27 + nltk/draw/cfg.py | 859 ++ nltk/draw/dispersion.py | 67 + nltk/draw/table.py | 1178 +++ nltk/draw/tree.py | 1129 +++ nltk/draw/util.py | 2575 ++++++ nltk/featstruct.py | 2787 ++++++ nltk/grammar.py | 1744 ++++ nltk/help.py | 68 + nltk/huggingface/__init__.py | 9 + nltk/huggingface/dataset.py | 334 + nltk/inference/__init__.py | 24 + nltk/inference/api.py | 614 ++ nltk/inference/discourse.py | 651 ++ nltk/inference/mace.py | 383 + nltk/inference/nonmonotonic.py | 561 ++ nltk/inference/prover9.py | 507 ++ nltk/inference/resolution.py | 788 ++ nltk/inference/tableau.py | 749 ++ nltk/internals.py | 1233 +++ nltk/jsontags.py | 76 + nltk/langnames.py | 828 ++ nltk/lazyimport.py | 137 + nltk/lm/__init__.py | 235 + nltk/lm/api.py | 239 + nltk/lm/counter.py | 207 + nltk/lm/models.py | 159 + nltk/lm/preprocessing.py | 51 + nltk/lm/smoothing.py | 127 + nltk/lm/util.py | 19 + nltk/lm/vocabulary.py | 218 + nltk/metrics/__init__.py | 51 + nltk/metrics/agreement.py | 487 + nltk/metrics/aline.py | 1652 ++++ nltk/metrics/artstein_poesio_example.txt | 200 + nltk/metrics/association.py | 476 + nltk/metrics/confusionmatrix.py | 380 + nltk/metrics/distance.py | 553 ++ nltk/metrics/paice.py | 389 + nltk/metrics/scores.py | 228 + nltk/metrics/segmentation.py | 267 + nltk/metrics/spearman.py | 68 + nltk/misc/__init__.py | 11 + nltk/misc/babelfish.py | 10 + nltk/misc/chomsky.py | 134 + nltk/misc/minimalset.py | 85 + nltk/misc/sort.py | 176 + nltk/misc/wordfinder.py | 139 + nltk/parse/__init__.py | 102 + nltk/parse/api.py | 72 + nltk/parse/bllip.py | 299 + nltk/parse/broker_test.cfg | 9 + nltk/parse/chart.py | 1907 ++++ nltk/parse/corenlp.py | 824 ++ nltk/parse/dependencygraph.py | 821 ++ nltk/parse/earleychart.py | 552 ++ nltk/parse/evaluate.py | 129 + nltk/parse/featurechart.py | 674 ++ nltk/parse/generate.py | 132 + nltk/parse/malt.py | 407 + nltk/parse/nonprojectivedependencyparser.py | 772 ++ nltk/parse/pchart.py | 579 ++ nltk/parse/projectivedependencyparser.py | 745 ++ nltk/parse/recursivedescent.py | 684 ++ nltk/parse/shiftreduce.py | 478 + nltk/parse/stanford.py | 471 + nltk/parse/test.cfg | 9 + nltk/parse/transitionparser.py | 809 ++ nltk/parse/util.py | 234 + nltk/parse/viterbi.py | 453 + nltk/pathsec.py | 594 ++ nltk/picklesec.py | 143 + nltk/probability.py | 2578 ++++++ nltk/sem/__init__.py | 75 + nltk/sem/boxer.py | 1632 ++++ nltk/sem/chat80.py | 857 ++ nltk/sem/cooper_storage.py | 124 + nltk/sem/drt.py | 1516 ++++ nltk/sem/drt_glue_demo.py | 553 ++ nltk/sem/evaluate.py | 925 ++ nltk/sem/glue.py | 835 ++ nltk/sem/hole.py | 395 + nltk/sem/lfg.py | 261 + nltk/sem/linearlogic.py | 481 + nltk/sem/logic.py | 2109 +++++ nltk/sem/relextract.py | 539 ++ nltk/sem/skolemize.py | 148 + nltk/sem/util.py | 311 + nltk/sentiment/__init__.py | 13 + nltk/sentiment/sentiment_analyzer.py | 255 + nltk/sentiment/util.py | 887 ++ nltk/sentiment/vader.py | 643 ++ nltk/stem/__init__.py | 34 + nltk/stem/api.py | 27 + nltk/stem/arlstem.py | 361 + nltk/stem/arlstem2.py | 457 + nltk/stem/cistem.py | 234 + nltk/stem/isri.py | 395 + nltk/stem/lancaster.py | 342 + nltk/stem/porter.py | 724 ++ nltk/stem/regexp.py | 55 + nltk/stem/rslp.py | 137 + nltk/stem/snowball.py | 5921 ++++++++++++ nltk/stem/util.py | 25 + nltk/stem/wordnet.py | 89 + nltk/tabdata.py | 108 + nltk/tag/__init__.py | 187 + nltk/tag/api.py | 296 + nltk/tag/brill.py | 449 + nltk/tag/brill_trainer.py | 628 ++ nltk/tag/crf.py | 316 + nltk/tag/hmm.py | 1326 +++ nltk/tag/hunpos.py | 142 + nltk/tag/mapping.py | 145 + nltk/tag/perceptron.py | 407 + nltk/tag/senna.py | 134 + nltk/tag/sequential.py | 752 ++ nltk/tag/stanford.py | 245 + nltk/tag/tnt.py | 1119 +++ nltk/tag/util.py | 72 + nltk/tbl/__init__.py | 31 + nltk/tbl/api.py | 0 nltk/tbl/demo.py | 419 + nltk/tbl/erroranalysis.py | 38 + nltk/tbl/feature.py | 267 + nltk/tbl/rule.py | 319 + nltk/tbl/template.py | 325 + nltk/test/FX8.xml | 17 + nltk/test/Makefile | 28 + nltk/test/__init__.py | 18 + nltk/test/all.py | 26 + nltk/test/bleu.doctest | 29 + nltk/test/bnc.doctest | 60 + nltk/test/ccg.doctest | 376 + nltk/test/ccg_semantics.doctest | 552 ++ nltk/test/chat80.doctest | 232 + nltk/test/childes.doctest | 190 + nltk/test/childes_fixt.py | 13 + nltk/test/chunk.doctest | 372 + nltk/test/classify.doctest | 202 + nltk/test/classify_fixt.py | 5 + nltk/test/collections.doctest | 31 + nltk/test/collocations.doctest | 307 + nltk/test/concordance.doctest | 75 + nltk/test/conftest.py | 35 + nltk/test/corpus.doctest | 2336 +++++ nltk/test/crubadan.doctest | 65 + nltk/test/data.doctest | 390 + nltk/test/dependency.doctest | 241 + nltk/test/discourse.doctest | 552 ++ nltk/test/drt.doctest | 515 ++ nltk/test/featgram.doctest | 610 ++ nltk/test/featstruct.doctest | 1229 +++ nltk/test/floresta.txt | 7924 +++++++++++++++++ nltk/test/framenet.doctest | 288 + nltk/test/generate.doctest | 66 + nltk/test/gensim.doctest | 141 + nltk/test/gensim_fixt.py | 4 + nltk/test/gluesemantics.doctest | 383 + nltk/test/gluesemantics_malt.doctest | 69 + nltk/test/gluesemantics_malt_fixt.py | 9 + nltk/test/grammar.doctest | 88 + nltk/test/grammartestsuites.doctest | 109 + nltk/test/images/twitter_app1.tiff | Bin 0 -> 51896 bytes nltk/test/images/twitter_app2.tiff | Bin 0 -> 152012 bytes nltk/test/images/twitter_app3.tiff | Bin 0 -> 102274 bytes nltk/test/images/twitter_app4.png | Bin 0 -> 938639 bytes nltk/test/index.doctest | 100 + nltk/test/inference.doctest | 536 ++ nltk/test/internals.doctest | 227 + nltk/test/japanese.doctest | 48 + nltk/test/lm.doctest | 136 + nltk/test/logic.doctest | 1096 +++ nltk/test/meteor.doctest | 54 + nltk/test/metrics.doctest | 321 + nltk/test/misc.doctest | 118 + nltk/test/nonmonotonic.doctest | 293 + nltk/test/onto1.fol | 6 + nltk/test/paice.doctest | 35 + nltk/test/parse.doctest | 933 ++ nltk/test/portuguese.doctest_latin1 | 301 + nltk/test/portuguese_en.doctest | 572 ++ nltk/test/portuguese_en_fixt.py | 4 + nltk/test/probability.doctest | 306 + nltk/test/probability_fixt.py | 8 + nltk/test/propbank.doctest | 176 + nltk/test/pytest.ini | 13 + nltk/test/relextract.doctest | 263 + nltk/test/resolution.doctest | 222 + nltk/test/sem3.cfg | 13 + nltk/test/semantics.doctest | 667 ++ nltk/test/sentiment.doctest | 236 + nltk/test/sentiwordnet.doctest | 41 + nltk/test/setup_fixt.py | 26 + nltk/test/simple.doctest | 83 + nltk/test/stem.doctest | 105 + nltk/test/tag.doctest | 472 + nltk/test/test_filestring_sandbox.py | 72 + nltk/test/tokenize.doctest | 446 + nltk/test/toolbox.doctest | 306 + nltk/test/toy.cfg | 8 + nltk/test/translate.doctest | 240 + nltk/test/tree.doctest | 1223 +++ nltk/test/treeprettyprinter.doctest | 177 + nltk/test/treetransforms.doctest | 154 + nltk/test/twitter.ipynb | 1288 +++ nltk/test/unit/__init__.py | 0 nltk/test/unit/files/bad_oauth1-1.txt | 4 + nltk/test/unit/files/bad_oauth1-2.txt | 5 + nltk/test/unit/files/bad_oauth1-3.txt | 4 + nltk/test/unit/files/credentials.txt | 4 + .../tweets.20150430-223406.hashtag.csv.ref | 58 + .../tweets.20150430-223406.media.csv.ref | 23 + .../tweets.20150430-223406.place.csv.ref | 5 + ...s.20150430-223406.placeboundingbox.csv.ref | 5 + .../tweets.20150430-223406.retweet.csv.ref | 76 + .../files/tweets.20150430-223406.text.csv.ref | 131 + .../tweets.20150430-223406.tweet.csv.ref | 131 + .../files/tweets.20150430-223406.url.csv.ref | 31 + .../files/tweets.20150430-223406.user.csv.ref | 131 + ...tweets.20150430-223406.usermention.csv.ref | 118 + .../tweets.20150430-223406.userurl.csv.ref | 1 + nltk/test/unit/lm/__init__.py | 0 nltk/test/unit/lm/test_counter.py | 170 + nltk/test/unit/lm/test_models.py | 627 ++ nltk/test/unit/lm/test_preprocessing.py | 30 + nltk/test/unit/lm/test_vocabulary.py | 156 + nltk/test/unit/test_aline.py | 238 + nltk/test/unit/test_alpino.py | 142 + nltk/test/unit/test_bllip.py | 42 + nltk/test/unit/test_boxer_security.py | 91 + nltk/test/unit/test_brill.py | 34 + nltk/test/unit/test_ccg_dir.py | 128 + nltk/test/unit/test_ccg_lexicon_security.py | 83 + nltk/test/unit/test_cfd_mutation.py | 39 + nltk/test/unit/test_cfg2chomsky.py | 49 + nltk/test/unit/test_chart_parser.py | 99 + nltk/test/unit/test_childes_security.py | 73 + nltk/test/unit/test_chunk.py | 85 + nltk/test/unit/test_chunk_redos_security.py | 115 + nltk/test/unit/test_cistem.py | 83 + nltk/test/unit/test_classify.py | 50 + nltk/test/unit/test_cli.py | 60 + nltk/test/unit/test_collocations.py | 120 + nltk/test/unit/test_concordance.py | 98 + nltk/test/unit/test_confusionmatrix.py | 87 + nltk/test/unit/test_conll_cmudict_security.py | 123 + nltk/test/unit/test_corenlp.py | 1440 +++ nltk/test/unit/test_corpora.py | 275 + nltk/test/unit/test_corpus_reader.py | 90 + nltk/test/unit/test_corpus_util.py | 142 + nltk/test/unit/test_corpus_views.py | 48 + nltk/test/unit/test_crf.py | 125 + nltk/test/unit/test_data.py | 37 + nltk/test/unit/test_data_security.py | 296 + .../unit/test_dependencygraph_security.py | 57 + nltk/test/unit/test_disagreement.py | 174 + nltk/test/unit/test_distance.py | 468 + nltk/test/unit/test_downloader.py | 195 + nltk/test/unit/test_downloader_atomic.py | 390 + .../unit/test_downloader_symlink_security.py | 186 + nltk/test/unit/test_downloader_unzip.py | 414 + nltk/test/unit/test_downloader_xxe.py | 72 + nltk/test/unit/test_drt_anaphora.py | 72 + nltk/test/unit/test_everygrams_alloc.py | 119 + nltk/test/unit/test_featstruct_redos.py | 80 + nltk/test/unit/test_framenet_security.py | 160 + nltk/test/unit/test_freqdist.py | 7 + nltk/test/unit/test_generate.py | 113 + nltk/test/unit/test_hmm.py | 82 + nltk/test/unit/test_ieer.py | 17 + nltk/test/unit/test_inference_security.py | 175 + nltk/test/unit/test_internals.py | 95 + nltk/test/unit/test_json2csv_corpus.py | 213 + nltk/test/unit/test_json_serialization.py | 95 + nltk/test/unit/test_langnames.py | 147 + nltk/test/unit/test_logic.py | 87 + nltk/test/unit/test_logic_security.py | 47 + nltk/test/unit/test_malt.py | 99 + nltk/test/unit/test_malt_security.py | 82 + nltk/test/unit/test_markdown.py | 122 + nltk/test/unit/test_metrics.py | 66 + nltk/test/unit/test_naivebayes.py | 21 + nltk/test/unit/test_nkjp_security.py | 95 + nltk/test/unit/test_nombank.py | 27 + nltk/test/unit/test_nombank_security.py | 52 + nltk/test/unit/test_open_datafile.py | 82 + nltk/test/unit/test_pathsec.py | 668 ++ .../unit/test_pickle_allowlist_security.py | 134 + nltk/test/unit/test_pickle_load_warnings.py | 88 + nltk/test/unit/test_pl196x.py | 13 + nltk/test/unit/test_platform_detection.py | 54 + nltk/test/unit/test_pos_tag.py | 165 + .../unit/test_projectivedependencyparser.py | 83 + nltk/test/unit/test_propbank.py | 68 + nltk/test/unit/test_repp_security.py | 69 + nltk/test/unit/test_reviews_security.py | 94 + nltk/test/unit/test_ribes.py | 282 + nltk/test/unit/test_rte_classify.py | 94 + .../test_seekable_unicode_stream_reader.py | 86 + nltk/test/unit/test_segmentation.py | 236 + nltk/test/unit/test_sem_evaluate.py | 96 + nltk/test/unit/test_sem_util.py | 22 + nltk/test/unit/test_senna.py | 112 + nltk/test/unit/test_senna_security.py | 93 + nltk/test/unit/test_senseval_security.py | 90 + nltk/test/unit/test_stanford_java_wrappers.py | 221 + nltk/test/unit/test_stem.py | 183 + nltk/test/unit/test_tag.py | 23 + nltk/test/unit/test_text.py | 113 + nltk/test/unit/test_texttiling.py | 224 + nltk/test/unit/test_texttiling_security.py | 106 + nltk/test/unit/test_tgrep.py | 784 ++ nltk/test/unit/test_tnt.py | 491 + nltk/test/unit/test_tokenize.py | 1194 +++ nltk/test/unit/test_tree.py | 28 + nltk/test/unit/test_treetransforms.py | 95 + nltk/test/unit/test_twitter_auth.py | 77 + nltk/test/unit/test_util.py | 82 + nltk/test/unit/test_valuation_redos.py | 67 + nltk/test/unit/test_verbnet.py | 339 + nltk/test/unit/test_weka_security.py | 54 + nltk/test/unit/test_wordnet.py | 311 + .../test_xml_entity_expansion_security.py | 46 + nltk/test/unit/test_xmldocs_security.py | 86 + nltk/test/unit/test_zipbomb_security.py | 92 + nltk/test/unit/translate/__init__.py | 0 nltk/test/unit/translate/test_alignment.py | 89 + nltk/test/unit/translate/test_bleu.py | 418 + nltk/test/unit/translate/test_gdfa.py | 197 + nltk/test/unit/translate/test_ibm1.py | 73 + nltk/test/unit/translate/test_ibm2.py | 86 + nltk/test/unit/translate/test_ibm3.py | 105 + nltk/test/unit/translate/test_ibm4.py | 120 + nltk/test/unit/translate/test_ibm5.py | 160 + nltk/test/unit/translate/test_ibm_model.py | 269 + nltk/test/unit/translate/test_lepor.py | 86 + nltk/test/unit/translate/test_meteor.py | 115 + nltk/test/unit/translate/test_nist.py | 36 + .../translate/test_phrase_based_security.py | 127 + .../test/unit/translate/test_stack_decoder.py | 294 + nltk/test/util.doctest | 47 + nltk/test/wordnet.doctest | 897 ++ nltk/test/wordnet_lch.doctest | 53 + nltk/test/wsd.doctest | 68 + nltk/text.py | 824 ++ nltk/tgrep.py | 1040 +++ nltk/tokenize/__init__.py | 145 + nltk/tokenize/api.py | 84 + nltk/tokenize/casual.py | 458 + nltk/tokenize/destructive.py | 239 + nltk/tokenize/legality_principle.py | 147 + nltk/tokenize/mwe.py | 124 + nltk/tokenize/nist.py | 179 + nltk/tokenize/punkt.py | 1849 ++++ nltk/tokenize/regexp.py | 220 + nltk/tokenize/repp.py | 157 + nltk/tokenize/sexpr.py | 140 + nltk/tokenize/simple.py | 139 + nltk/tokenize/sonority_sequencing.py | 194 + nltk/tokenize/stanford.py | 123 + nltk/tokenize/stanford_segmenter.py | 341 + nltk/tokenize/texttiling.py | 563 ++ nltk/tokenize/toktok.py | 180 + nltk/tokenize/treebank.py | 409 + nltk/tokenize/util.py | 295 + nltk/toolbox.py | 524 ++ nltk/translate/__init__.py | 33 + nltk/translate/api.py | 353 + nltk/translate/bleu_score.py | 714 ++ nltk/translate/chrf_score.py | 221 + nltk/translate/gale_church.py | 263 + nltk/translate/gdfa.py | 141 + nltk/translate/gleu_score.py | 190 + nltk/translate/ibm1.py | 251 + nltk/translate/ibm2.py | 319 + nltk/translate/ibm3.py | 346 + nltk/translate/ibm4.py | 490 + nltk/translate/ibm5.py | 661 ++ nltk/translate/ibm_model.py | 549 ++ nltk/translate/lepor.py | 335 + nltk/translate/meteor_score.py | 455 + nltk/translate/metrics.py | 41 + nltk/translate/nist_score.py | 195 + nltk/translate/phrase_based.py | 227 + nltk/translate/ribes_score.py | 397 + nltk/translate/stack_decoder.py | 515 ++ nltk/tree/__init__.py | 52 + nltk/tree/immutable.py | 124 + nltk/tree/parented.py | 590 ++ nltk/tree/parsing.py | 66 + nltk/tree/prettyprinter.py | 627 ++ nltk/tree/probabilistic.py | 74 + nltk/tree/transforms.py | 344 + nltk/tree/tree.py | 987 ++ nltk/treeprettyprinter.py | 28 + nltk/treetransforms.py | 126 + nltk/twitter/__init__.py | 35 + nltk/twitter/api.py | 145 + nltk/twitter/common.py | 270 + nltk/twitter/twitter_demo.py | 306 + nltk/twitter/twitterclient.py | 562 ++ nltk/twitter/util.py | 147 + nltk/util.py | 1375 +++ nltk/wsd.py | 52 + pip-req.txt | 15 + requirements-ci.txt | 15 + requirements-test.txt | 5 + ruff.toml | 19 + setup.cfg | 8 + setup.py | 126 + tools/find_deprecated.py | 263 + tools/github_actions/third-party.sh | 129 + tools/global_replace.py | 60 + tools/huggingface/push_stopwords.py | 226 + tox.ini | 123 + web/Makefile | 160 + web/_static/css/team.css | 38 + web/_static/images/book.gif | Bin 0 -> 7170 bytes web/_static/images/tree.gif | Bin 0 -> 14369 bytes web/_templates/doctest.rst | 11 + web/_templates/module.rst | 16 + web/_templates/package.rst | 37 + web/_templates/team.html | 58 + web/api/.gitkeep | 1 + web/conf.py | 341 + web/contribute.rst | 13 + web/data.rst | 62 + web/dev/jenkins.rst | 109 + web/dev/local_testing.rst | 155 + web/howto.rst | 8 + web/index.rst | 101 + web/install.rst | 50 + web/news.rst | 676 ++ web/py-modindex.rst | 6 + web/team.rst | 11 + web/team/current_team.html | 326 + web/team/former_team.html | 348 + web/team/team.json | 173 + 576 files changed, 191861 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/dependabot.yml create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/cffconvert.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/retest-pr.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 AUTHORS.md create mode 100644 CITATION.cff create mode 100644 CONTRIBUTING.md create mode 100644 ChangeLog create mode 100644 LICENSE.txt create mode 100644 MANIFEST.in create mode 100644 Makefile create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 RELEASE-Github.md create mode 100644 RELEASE-HOWTO.txt create mode 100644 SECURITY.md create mode 100644 nltk/VERSION create mode 100644 nltk/__init__.py create mode 100644 nltk/app/__init__.py create mode 100644 nltk/app/chartparser_app.py create mode 100644 nltk/app/chunkparser_app.py create mode 100644 nltk/app/collocations_app.py create mode 100755 nltk/app/concordance_app.py create mode 100755 nltk/app/nemo_app.py create mode 100644 nltk/app/rdparser_app.py create mode 100644 nltk/app/srparser_app.py create mode 100644 nltk/app/wordfreq_app.py create mode 100644 nltk/app/wordnet_app.py create mode 100644 nltk/book.py create mode 100644 nltk/ccg/__init__.py create mode 100644 nltk/ccg/api.py create mode 100644 nltk/ccg/chart.py create mode 100644 nltk/ccg/combinator.py create mode 100644 nltk/ccg/lexicon.py create mode 100644 nltk/ccg/logic.py create mode 100644 nltk/chat/__init__.py create mode 100644 nltk/chat/eliza.py create mode 100644 nltk/chat/iesha.py create mode 100644 nltk/chat/rude.py create mode 100644 nltk/chat/suntsu.py create mode 100644 nltk/chat/util.py create mode 100644 nltk/chat/zen.py create mode 100644 nltk/chunk/__init__.py create mode 100644 nltk/chunk/api.py create mode 100644 nltk/chunk/named_entity.py create mode 100644 nltk/chunk/regexp.py create mode 100644 nltk/chunk/util.py create mode 100644 nltk/classify/__init__.py create mode 100644 nltk/classify/api.py create mode 100644 nltk/classify/decisiontree.py create mode 100644 nltk/classify/maxent.py create mode 100644 nltk/classify/megam.py create mode 100644 nltk/classify/naivebayes.py create mode 100644 nltk/classify/positivenaivebayes.py create mode 100644 nltk/classify/rte_classify.py create mode 100644 nltk/classify/scikitlearn.py create mode 100644 nltk/classify/senna.py create mode 100644 nltk/classify/svm.py create mode 100644 nltk/classify/tadm.py create mode 100644 nltk/classify/textcat.py create mode 100644 nltk/classify/util.py create mode 100644 nltk/classify/weka.py create mode 100644 nltk/cli.py create mode 100644 nltk/cluster/__init__.py create mode 100644 nltk/cluster/api.py create mode 100644 nltk/cluster/em.py create mode 100644 nltk/cluster/gaac.py create mode 100644 nltk/cluster/kmeans.py create mode 100644 nltk/cluster/util.py create mode 100644 nltk/collections.py create mode 100644 nltk/collocations.py create mode 100755 nltk/compat.py create mode 100644 nltk/corpus/__init__.py create mode 100644 nltk/corpus/europarl_raw.py create mode 100644 nltk/corpus/reader/__init__.py create mode 100644 nltk/corpus/reader/aligned.py create mode 100644 nltk/corpus/reader/api.py create mode 100644 nltk/corpus/reader/bcp47.py create mode 100644 nltk/corpus/reader/bnc.py create mode 100644 nltk/corpus/reader/bracket_parse.py create mode 100644 nltk/corpus/reader/categorized_sents.py create mode 100644 nltk/corpus/reader/chasen.py create mode 100644 nltk/corpus/reader/childes.py create mode 100644 nltk/corpus/reader/chunked.py create mode 100644 nltk/corpus/reader/cmudict.py create mode 100644 nltk/corpus/reader/comparative_sents.py create mode 100644 nltk/corpus/reader/conll.py create mode 100644 nltk/corpus/reader/crubadan.py create mode 100644 nltk/corpus/reader/dependency.py create mode 100644 nltk/corpus/reader/framenet.py create mode 100644 nltk/corpus/reader/ieer.py create mode 100644 nltk/corpus/reader/indian.py create mode 100644 nltk/corpus/reader/ipipan.py create mode 100644 nltk/corpus/reader/knbc.py create mode 100644 nltk/corpus/reader/lin.py create mode 100644 nltk/corpus/reader/markdown.py create mode 100644 nltk/corpus/reader/mte.py create mode 100644 nltk/corpus/reader/nkjp.py create mode 100644 nltk/corpus/reader/nombank.py create mode 100644 nltk/corpus/reader/nps_chat.py create mode 100644 nltk/corpus/reader/opinion_lexicon.py create mode 100644 nltk/corpus/reader/panlex_lite.py create mode 100644 nltk/corpus/reader/panlex_swadesh.py create mode 100644 nltk/corpus/reader/pl196x.py create mode 100644 nltk/corpus/reader/plaintext.py create mode 100644 nltk/corpus/reader/ppattach.py create mode 100644 nltk/corpus/reader/propbank.py create mode 100644 nltk/corpus/reader/pros_cons.py create mode 100644 nltk/corpus/reader/reviews.py create mode 100644 nltk/corpus/reader/rte.py create mode 100644 nltk/corpus/reader/semcor.py create mode 100644 nltk/corpus/reader/senseval.py create mode 100644 nltk/corpus/reader/sentiwordnet.py create mode 100644 nltk/corpus/reader/sinica_treebank.py create mode 100644 nltk/corpus/reader/string_category.py create mode 100644 nltk/corpus/reader/switchboard.py create mode 100644 nltk/corpus/reader/tagged.py create mode 100644 nltk/corpus/reader/timit.py create mode 100644 nltk/corpus/reader/toolbox.py create mode 100644 nltk/corpus/reader/twitter.py create mode 100644 nltk/corpus/reader/udhr.py create mode 100644 nltk/corpus/reader/util.py create mode 100644 nltk/corpus/reader/verbnet.py create mode 100644 nltk/corpus/reader/wordlist.py create mode 100644 nltk/corpus/reader/wordnet.py create mode 100644 nltk/corpus/reader/xmldocs.py create mode 100644 nltk/corpus/reader/ycoe.py create mode 100644 nltk/corpus/util.py create mode 100644 nltk/data.py create mode 100644 nltk/decorators.py create mode 100644 nltk/downloader.py create mode 100644 nltk/draw/__init__.py create mode 100644 nltk/draw/cfg.py create mode 100644 nltk/draw/dispersion.py create mode 100644 nltk/draw/table.py create mode 100644 nltk/draw/tree.py create mode 100644 nltk/draw/util.py create mode 100644 nltk/featstruct.py create mode 100644 nltk/grammar.py create mode 100644 nltk/help.py create mode 100644 nltk/huggingface/__init__.py create mode 100644 nltk/huggingface/dataset.py create mode 100644 nltk/inference/__init__.py create mode 100644 nltk/inference/api.py create mode 100644 nltk/inference/discourse.py create mode 100644 nltk/inference/mace.py create mode 100644 nltk/inference/nonmonotonic.py create mode 100644 nltk/inference/prover9.py create mode 100755 nltk/inference/resolution.py create mode 100644 nltk/inference/tableau.py create mode 100644 nltk/internals.py create mode 100644 nltk/jsontags.py create mode 100644 nltk/langnames.py create mode 100644 nltk/lazyimport.py create mode 100644 nltk/lm/__init__.py create mode 100644 nltk/lm/api.py create mode 100644 nltk/lm/counter.py create mode 100644 nltk/lm/models.py create mode 100644 nltk/lm/preprocessing.py create mode 100644 nltk/lm/smoothing.py create mode 100644 nltk/lm/util.py create mode 100644 nltk/lm/vocabulary.py create mode 100644 nltk/metrics/__init__.py create mode 100644 nltk/metrics/agreement.py create mode 100644 nltk/metrics/aline.py create mode 100644 nltk/metrics/artstein_poesio_example.txt create mode 100644 nltk/metrics/association.py create mode 100644 nltk/metrics/confusionmatrix.py create mode 100644 nltk/metrics/distance.py create mode 100644 nltk/metrics/paice.py create mode 100644 nltk/metrics/scores.py create mode 100644 nltk/metrics/segmentation.py create mode 100644 nltk/metrics/spearman.py create mode 100644 nltk/misc/__init__.py create mode 100644 nltk/misc/babelfish.py create mode 100644 nltk/misc/chomsky.py create mode 100644 nltk/misc/minimalset.py create mode 100644 nltk/misc/sort.py create mode 100644 nltk/misc/wordfinder.py create mode 100644 nltk/parse/__init__.py create mode 100644 nltk/parse/api.py create mode 100644 nltk/parse/bllip.py create mode 100644 nltk/parse/broker_test.cfg create mode 100644 nltk/parse/chart.py create mode 100644 nltk/parse/corenlp.py create mode 100755 nltk/parse/dependencygraph.py create mode 100644 nltk/parse/earleychart.py create mode 100644 nltk/parse/evaluate.py create mode 100644 nltk/parse/featurechart.py create mode 100644 nltk/parse/generate.py create mode 100644 nltk/parse/malt.py create mode 100644 nltk/parse/nonprojectivedependencyparser.py create mode 100644 nltk/parse/pchart.py create mode 100644 nltk/parse/projectivedependencyparser.py create mode 100644 nltk/parse/recursivedescent.py create mode 100644 nltk/parse/shiftreduce.py create mode 100644 nltk/parse/stanford.py create mode 100644 nltk/parse/test.cfg create mode 100644 nltk/parse/transitionparser.py create mode 100644 nltk/parse/util.py create mode 100644 nltk/parse/viterbi.py create mode 100644 nltk/pathsec.py create mode 100644 nltk/picklesec.py create mode 100755 nltk/probability.py create mode 100644 nltk/sem/__init__.py create mode 100644 nltk/sem/boxer.py create mode 100644 nltk/sem/chat80.py create mode 100644 nltk/sem/cooper_storage.py create mode 100644 nltk/sem/drt.py create mode 100644 nltk/sem/drt_glue_demo.py create mode 100644 nltk/sem/evaluate.py create mode 100644 nltk/sem/glue.py create mode 100644 nltk/sem/hole.py create mode 100644 nltk/sem/lfg.py create mode 100644 nltk/sem/linearlogic.py create mode 100644 nltk/sem/logic.py create mode 100644 nltk/sem/relextract.py create mode 100644 nltk/sem/skolemize.py create mode 100644 nltk/sem/util.py create mode 100644 nltk/sentiment/__init__.py create mode 100644 nltk/sentiment/sentiment_analyzer.py create mode 100644 nltk/sentiment/util.py create mode 100644 nltk/sentiment/vader.py create mode 100644 nltk/stem/__init__.py create mode 100644 nltk/stem/api.py create mode 100644 nltk/stem/arlstem.py create mode 100644 nltk/stem/arlstem2.py create mode 100644 nltk/stem/cistem.py create mode 100644 nltk/stem/isri.py create mode 100644 nltk/stem/lancaster.py create mode 100644 nltk/stem/porter.py create mode 100644 nltk/stem/regexp.py create mode 100644 nltk/stem/rslp.py create mode 100644 nltk/stem/snowball.py create mode 100644 nltk/stem/util.py create mode 100644 nltk/stem/wordnet.py create mode 100644 nltk/tabdata.py create mode 100644 nltk/tag/__init__.py create mode 100644 nltk/tag/api.py create mode 100644 nltk/tag/brill.py create mode 100644 nltk/tag/brill_trainer.py create mode 100644 nltk/tag/crf.py create mode 100644 nltk/tag/hmm.py create mode 100644 nltk/tag/hunpos.py create mode 100644 nltk/tag/mapping.py create mode 100644 nltk/tag/perceptron.py create mode 100644 nltk/tag/senna.py create mode 100644 nltk/tag/sequential.py create mode 100644 nltk/tag/stanford.py create mode 100755 nltk/tag/tnt.py create mode 100644 nltk/tag/util.py create mode 100644 nltk/tbl/__init__.py create mode 100644 nltk/tbl/api.py create mode 100644 nltk/tbl/demo.py create mode 100644 nltk/tbl/erroranalysis.py create mode 100644 nltk/tbl/feature.py create mode 100644 nltk/tbl/rule.py create mode 100644 nltk/tbl/template.py create mode 100644 nltk/test/FX8.xml create mode 100644 nltk/test/Makefile create mode 100644 nltk/test/__init__.py create mode 100644 nltk/test/all.py create mode 100644 nltk/test/bleu.doctest create mode 100644 nltk/test/bnc.doctest create mode 100644 nltk/test/ccg.doctest create mode 100644 nltk/test/ccg_semantics.doctest create mode 100644 nltk/test/chat80.doctest create mode 100644 nltk/test/childes.doctest create mode 100644 nltk/test/childes_fixt.py create mode 100644 nltk/test/chunk.doctest create mode 100644 nltk/test/classify.doctest create mode 100644 nltk/test/classify_fixt.py create mode 100644 nltk/test/collections.doctest create mode 100644 nltk/test/collocations.doctest create mode 100644 nltk/test/concordance.doctest create mode 100644 nltk/test/conftest.py create mode 100644 nltk/test/corpus.doctest create mode 100644 nltk/test/crubadan.doctest create mode 100644 nltk/test/data.doctest create mode 100755 nltk/test/dependency.doctest create mode 100644 nltk/test/discourse.doctest create mode 100644 nltk/test/drt.doctest create mode 100644 nltk/test/featgram.doctest create mode 100644 nltk/test/featstruct.doctest create mode 100644 nltk/test/floresta.txt create mode 100644 nltk/test/framenet.doctest create mode 100644 nltk/test/generate.doctest create mode 100644 nltk/test/gensim.doctest create mode 100644 nltk/test/gensim_fixt.py create mode 100644 nltk/test/gluesemantics.doctest create mode 100644 nltk/test/gluesemantics_malt.doctest create mode 100644 nltk/test/gluesemantics_malt_fixt.py create mode 100644 nltk/test/grammar.doctest create mode 100644 nltk/test/grammartestsuites.doctest create mode 100644 nltk/test/images/twitter_app1.tiff create mode 100644 nltk/test/images/twitter_app2.tiff create mode 100644 nltk/test/images/twitter_app3.tiff create mode 100644 nltk/test/images/twitter_app4.png create mode 100644 nltk/test/index.doctest create mode 100644 nltk/test/inference.doctest create mode 100644 nltk/test/internals.doctest create mode 100644 nltk/test/japanese.doctest create mode 100644 nltk/test/lm.doctest create mode 100644 nltk/test/logic.doctest create mode 100644 nltk/test/meteor.doctest create mode 100644 nltk/test/metrics.doctest create mode 100644 nltk/test/misc.doctest create mode 100644 nltk/test/nonmonotonic.doctest create mode 100644 nltk/test/onto1.fol create mode 100644 nltk/test/paice.doctest create mode 100644 nltk/test/parse.doctest create mode 100644 nltk/test/portuguese.doctest_latin1 create mode 100644 nltk/test/portuguese_en.doctest create mode 100644 nltk/test/portuguese_en_fixt.py create mode 100644 nltk/test/probability.doctest create mode 100644 nltk/test/probability_fixt.py create mode 100644 nltk/test/propbank.doctest create mode 100644 nltk/test/pytest.ini create mode 100644 nltk/test/relextract.doctest create mode 100644 nltk/test/resolution.doctest create mode 100644 nltk/test/sem3.cfg create mode 100644 nltk/test/semantics.doctest create mode 100644 nltk/test/sentiment.doctest create mode 100644 nltk/test/sentiwordnet.doctest create mode 100644 nltk/test/setup_fixt.py create mode 100644 nltk/test/simple.doctest create mode 100644 nltk/test/stem.doctest create mode 100644 nltk/test/tag.doctest create mode 100644 nltk/test/test_filestring_sandbox.py create mode 100644 nltk/test/tokenize.doctest create mode 100644 nltk/test/toolbox.doctest create mode 100644 nltk/test/toy.cfg create mode 100644 nltk/test/translate.doctest create mode 100644 nltk/test/tree.doctest create mode 100644 nltk/test/treeprettyprinter.doctest create mode 100644 nltk/test/treetransforms.doctest create mode 100644 nltk/test/twitter.ipynb create mode 100644 nltk/test/unit/__init__.py create mode 100644 nltk/test/unit/files/bad_oauth1-1.txt create mode 100644 nltk/test/unit/files/bad_oauth1-2.txt create mode 100644 nltk/test/unit/files/bad_oauth1-3.txt create mode 100644 nltk/test/unit/files/credentials.txt create mode 100644 nltk/test/unit/files/tweets.20150430-223406.hashtag.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.media.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.place.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.placeboundingbox.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.retweet.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.text.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.tweet.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.url.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.user.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.usermention.csv.ref create mode 100644 nltk/test/unit/files/tweets.20150430-223406.userurl.csv.ref create mode 100644 nltk/test/unit/lm/__init__.py create mode 100644 nltk/test/unit/lm/test_counter.py create mode 100644 nltk/test/unit/lm/test_models.py create mode 100644 nltk/test/unit/lm/test_preprocessing.py create mode 100644 nltk/test/unit/lm/test_vocabulary.py create mode 100644 nltk/test/unit/test_aline.py create mode 100644 nltk/test/unit/test_alpino.py create mode 100644 nltk/test/unit/test_bllip.py create mode 100644 nltk/test/unit/test_boxer_security.py create mode 100644 nltk/test/unit/test_brill.py create mode 100644 nltk/test/unit/test_ccg_dir.py create mode 100644 nltk/test/unit/test_ccg_lexicon_security.py create mode 100644 nltk/test/unit/test_cfd_mutation.py create mode 100644 nltk/test/unit/test_cfg2chomsky.py create mode 100644 nltk/test/unit/test_chart_parser.py create mode 100644 nltk/test/unit/test_childes_security.py create mode 100644 nltk/test/unit/test_chunk.py create mode 100644 nltk/test/unit/test_chunk_redos_security.py create mode 100644 nltk/test/unit/test_cistem.py create mode 100644 nltk/test/unit/test_classify.py create mode 100644 nltk/test/unit/test_cli.py create mode 100644 nltk/test/unit/test_collocations.py create mode 100644 nltk/test/unit/test_concordance.py create mode 100644 nltk/test/unit/test_confusionmatrix.py create mode 100644 nltk/test/unit/test_conll_cmudict_security.py create mode 100644 nltk/test/unit/test_corenlp.py create mode 100644 nltk/test/unit/test_corpora.py create mode 100644 nltk/test/unit/test_corpus_reader.py create mode 100644 nltk/test/unit/test_corpus_util.py create mode 100644 nltk/test/unit/test_corpus_views.py create mode 100644 nltk/test/unit/test_crf.py create mode 100644 nltk/test/unit/test_data.py create mode 100644 nltk/test/unit/test_data_security.py create mode 100644 nltk/test/unit/test_dependencygraph_security.py create mode 100644 nltk/test/unit/test_disagreement.py create mode 100644 nltk/test/unit/test_distance.py create mode 100644 nltk/test/unit/test_downloader.py create mode 100644 nltk/test/unit/test_downloader_atomic.py create mode 100644 nltk/test/unit/test_downloader_symlink_security.py create mode 100644 nltk/test/unit/test_downloader_unzip.py create mode 100644 nltk/test/unit/test_downloader_xxe.py create mode 100644 nltk/test/unit/test_drt_anaphora.py create mode 100644 nltk/test/unit/test_everygrams_alloc.py create mode 100644 nltk/test/unit/test_featstruct_redos.py create mode 100644 nltk/test/unit/test_framenet_security.py create mode 100644 nltk/test/unit/test_freqdist.py create mode 100644 nltk/test/unit/test_generate.py create mode 100644 nltk/test/unit/test_hmm.py create mode 100644 nltk/test/unit/test_ieer.py create mode 100644 nltk/test/unit/test_inference_security.py create mode 100644 nltk/test/unit/test_internals.py create mode 100644 nltk/test/unit/test_json2csv_corpus.py create mode 100644 nltk/test/unit/test_json_serialization.py create mode 100644 nltk/test/unit/test_langnames.py create mode 100644 nltk/test/unit/test_logic.py create mode 100644 nltk/test/unit/test_logic_security.py create mode 100644 nltk/test/unit/test_malt.py create mode 100644 nltk/test/unit/test_malt_security.py create mode 100644 nltk/test/unit/test_markdown.py create mode 100644 nltk/test/unit/test_metrics.py create mode 100644 nltk/test/unit/test_naivebayes.py create mode 100644 nltk/test/unit/test_nkjp_security.py create mode 100644 nltk/test/unit/test_nombank.py create mode 100644 nltk/test/unit/test_nombank_security.py create mode 100644 nltk/test/unit/test_open_datafile.py create mode 100644 nltk/test/unit/test_pathsec.py create mode 100644 nltk/test/unit/test_pickle_allowlist_security.py create mode 100644 nltk/test/unit/test_pickle_load_warnings.py create mode 100644 nltk/test/unit/test_pl196x.py create mode 100644 nltk/test/unit/test_platform_detection.py create mode 100644 nltk/test/unit/test_pos_tag.py create mode 100644 nltk/test/unit/test_projectivedependencyparser.py create mode 100644 nltk/test/unit/test_propbank.py create mode 100644 nltk/test/unit/test_repp_security.py create mode 100644 nltk/test/unit/test_reviews_security.py create mode 100644 nltk/test/unit/test_ribes.py create mode 100644 nltk/test/unit/test_rte_classify.py create mode 100644 nltk/test/unit/test_seekable_unicode_stream_reader.py create mode 100644 nltk/test/unit/test_segmentation.py create mode 100644 nltk/test/unit/test_sem_evaluate.py create mode 100644 nltk/test/unit/test_sem_util.py create mode 100644 nltk/test/unit/test_senna.py create mode 100644 nltk/test/unit/test_senna_security.py create mode 100644 nltk/test/unit/test_senseval_security.py create mode 100644 nltk/test/unit/test_stanford_java_wrappers.py create mode 100644 nltk/test/unit/test_stem.py create mode 100644 nltk/test/unit/test_tag.py create mode 100644 nltk/test/unit/test_text.py create mode 100644 nltk/test/unit/test_texttiling.py create mode 100644 nltk/test/unit/test_texttiling_security.py create mode 100644 nltk/test/unit/test_tgrep.py create mode 100644 nltk/test/unit/test_tnt.py create mode 100644 nltk/test/unit/test_tokenize.py create mode 100644 nltk/test/unit/test_tree.py create mode 100644 nltk/test/unit/test_treetransforms.py create mode 100644 nltk/test/unit/test_twitter_auth.py create mode 100644 nltk/test/unit/test_util.py create mode 100644 nltk/test/unit/test_valuation_redos.py create mode 100644 nltk/test/unit/test_verbnet.py create mode 100644 nltk/test/unit/test_weka_security.py create mode 100644 nltk/test/unit/test_wordnet.py create mode 100644 nltk/test/unit/test_xml_entity_expansion_security.py create mode 100644 nltk/test/unit/test_xmldocs_security.py create mode 100644 nltk/test/unit/test_zipbomb_security.py create mode 100644 nltk/test/unit/translate/__init__.py create mode 100644 nltk/test/unit/translate/test_alignment.py create mode 100644 nltk/test/unit/translate/test_bleu.py create mode 100644 nltk/test/unit/translate/test_gdfa.py create mode 100644 nltk/test/unit/translate/test_ibm1.py create mode 100644 nltk/test/unit/translate/test_ibm2.py create mode 100644 nltk/test/unit/translate/test_ibm3.py create mode 100644 nltk/test/unit/translate/test_ibm4.py create mode 100644 nltk/test/unit/translate/test_ibm5.py create mode 100644 nltk/test/unit/translate/test_ibm_model.py create mode 100644 nltk/test/unit/translate/test_lepor.py create mode 100644 nltk/test/unit/translate/test_meteor.py create mode 100644 nltk/test/unit/translate/test_nist.py create mode 100644 nltk/test/unit/translate/test_phrase_based_security.py create mode 100644 nltk/test/unit/translate/test_stack_decoder.py create mode 100644 nltk/test/util.doctest create mode 100644 nltk/test/wordnet.doctest create mode 100644 nltk/test/wordnet_lch.doctest create mode 100644 nltk/test/wsd.doctest create mode 100644 nltk/text.py create mode 100644 nltk/tgrep.py create mode 100644 nltk/tokenize/__init__.py create mode 100644 nltk/tokenize/api.py create mode 100644 nltk/tokenize/casual.py create mode 100644 nltk/tokenize/destructive.py create mode 100644 nltk/tokenize/legality_principle.py create mode 100644 nltk/tokenize/mwe.py create mode 100644 nltk/tokenize/nist.py create mode 100644 nltk/tokenize/punkt.py create mode 100644 nltk/tokenize/regexp.py create mode 100644 nltk/tokenize/repp.py create mode 100644 nltk/tokenize/sexpr.py create mode 100644 nltk/tokenize/simple.py create mode 100644 nltk/tokenize/sonority_sequencing.py create mode 100644 nltk/tokenize/stanford.py create mode 100644 nltk/tokenize/stanford_segmenter.py create mode 100644 nltk/tokenize/texttiling.py create mode 100644 nltk/tokenize/toktok.py create mode 100644 nltk/tokenize/treebank.py create mode 100644 nltk/tokenize/util.py create mode 100644 nltk/toolbox.py create mode 100644 nltk/translate/__init__.py create mode 100644 nltk/translate/api.py create mode 100644 nltk/translate/bleu_score.py create mode 100644 nltk/translate/chrf_score.py create mode 100644 nltk/translate/gale_church.py create mode 100644 nltk/translate/gdfa.py create mode 100644 nltk/translate/gleu_score.py create mode 100644 nltk/translate/ibm1.py create mode 100644 nltk/translate/ibm2.py create mode 100644 nltk/translate/ibm3.py create mode 100644 nltk/translate/ibm4.py create mode 100644 nltk/translate/ibm5.py create mode 100644 nltk/translate/ibm_model.py create mode 100644 nltk/translate/lepor.py create mode 100644 nltk/translate/meteor_score.py create mode 100644 nltk/translate/metrics.py create mode 100644 nltk/translate/nist_score.py create mode 100644 nltk/translate/phrase_based.py create mode 100644 nltk/translate/ribes_score.py create mode 100644 nltk/translate/stack_decoder.py create mode 100644 nltk/tree/__init__.py create mode 100644 nltk/tree/immutable.py create mode 100644 nltk/tree/parented.py create mode 100644 nltk/tree/parsing.py create mode 100644 nltk/tree/prettyprinter.py create mode 100644 nltk/tree/probabilistic.py create mode 100644 nltk/tree/transforms.py create mode 100644 nltk/tree/tree.py create mode 100644 nltk/treeprettyprinter.py create mode 100644 nltk/treetransforms.py create mode 100644 nltk/twitter/__init__.py create mode 100644 nltk/twitter/api.py create mode 100644 nltk/twitter/common.py create mode 100644 nltk/twitter/twitter_demo.py create mode 100644 nltk/twitter/twitterclient.py create mode 100644 nltk/twitter/util.py create mode 100644 nltk/util.py create mode 100644 nltk/wsd.py create mode 100644 pip-req.txt create mode 100644 requirements-ci.txt create mode 100644 requirements-test.txt create mode 100644 ruff.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100755 tools/find_deprecated.py create mode 100644 tools/github_actions/third-party.sh create mode 100755 tools/global_replace.py create mode 100644 tools/huggingface/push_stopwords.py create mode 100644 tox.ini create mode 100644 web/Makefile create mode 100644 web/_static/css/team.css create mode 100644 web/_static/images/book.gif create mode 100644 web/_static/images/tree.gif create mode 100644 web/_templates/doctest.rst create mode 100644 web/_templates/module.rst create mode 100644 web/_templates/package.rst create mode 100644 web/_templates/team.html create mode 100644 web/api/.gitkeep create mode 100644 web/conf.py create mode 100644 web/contribute.rst create mode 100644 web/data.rst create mode 100644 web/dev/jenkins.rst create mode 100644 web/dev/local_testing.rst create mode 100644 web/howto.rst create mode 100644 web/index.rst create mode 100644 web/install.rst create mode 100644 web/news.rst create mode 100644 web/py-modindex.rst create mode 100644 web/team.rst create mode 100644 web/team/current_team.html create mode 100644 web/team/former_team.html create mode 100644 web/team/team.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8b69a05 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + # Enable version updates for GitHub Actions + # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + open-pull-requests-limit: 5 + groups: + # Group all GitHub Actions updates into a single PR + github-actions: + patterns: + - "*" + commit-message: + prefix: ci + include: scope diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..6e93634 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,64 @@ +# https://github.com/actions/labeler +CI: + - changed-files: + - any-glob-to-any-file: [".github/workflows/*", ".github/labeler.yml"] + +classifier: + - changed-files: + - any-glob-to-any-file: "nltk/classify/**/*" + +cli: + - changed-files: + - any-glob-to-any-file: "nltk/cli.py" + +cluster: + - changed-files: + - any-glob-to-any-file: "nltk/cluster/**/*" + +corpus: + - changed-files: + - any-glob-to-any-file: "nltk/corpus/**/*" + +GUI: + - changed-files: + - any-glob-to-any-file: "nltk/app/**/*" + +internals: + - changed-files: + - any-glob-to-any-file: "nltk/internals.py" + +language-model: + - changed-files: + - any-glob-to-any-file: "nltk/lm/**/*" + +metrics: + - changed-files: + - any-glob-to-any-file: "nltk/metrics/**/*" + +parsing: + - changed-files: + - any-glob-to-any-file: "nltk/parse/**/*" + +sentiment: + - changed-files: + - any-glob-to-any-file: "nltk/sentiment/**/*" + +stem/lemma: + - changed-files: + - any-glob-to-any-file: "nltk/stem/**/*" + +tagger: + - changed-files: + - any-glob-to-any-file: "nltk/tag/**/*" + +tokenizer: + - changed-files: + - any-glob-to-any-file: "nltk/tokenize/**/*" + +twitter: + - changed-files: + - any-glob-to-any-file: "nltk/twitter/**/*" + +wordnet: + - changed-files: + - any-glob-to-any-file: "nltk/wordnet/**/*" diff --git a/.github/workflows/cffconvert.yml b/.github/workflows/cffconvert.yml new file mode 100644 index 0000000..6eb74bc --- /dev/null +++ b/.github/workflows/cffconvert.yml @@ -0,0 +1,23 @@ +name: cffconvert + +on: + push: + paths: + - CITATION.cff + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + name: "validate" + runs-on: ubuntu-latest + steps: + - name: Check out a copy of the repository + uses: actions/checkout@v7 + + - name: Check whether the citation metadata from CITATION.cff is valid + uses: citation-file-format/cffconvert-github-action@2.0.0 + with: + args: "--validate" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c0caee8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,160 @@ +name: ci-workflow + +on: [push, pull_request, workflow_dispatch] + +permissions: + contents: read + +env: + THIRD_PARTY_DIR: ${{ github.workspace }}/third + CORENLP: ${{ github.workspace }}/third/stanford-corenlp + CORENLP_MODELS: ${{ github.workspace }}/third/stanford-corenlp + STANFORD_PARSER: ${{ github.workspace }}/third/stanford-parser + STANFORD_MODELS: ${{ github.workspace }}/third/stanford-postagger + STANFORD_POSTAGGER: ${{ github.workspace }}/third/stanford-postagger + SENNA: ${{ github.workspace }}/third/senna + PROVER9: ${{ github.workspace }}/third/prover9/bin + MEGAM: ${{ github.workspace }}/third/megam + MALT_PARSER: ${{ github.workspace }}/third/maltparser + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" # or your chosen version + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit hooks + run: pre-commit run --all-files + + minimal_download_test: + name: Minimal NLTK Download Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: Install core dependencies + run: pip install regex defusedxml + + - name: Set NLTK_DATA environment variable + shell: bash + run: echo "NLTK_DATA=${{ github.workspace }}/nltk_data" >> $GITHUB_ENV + + - name: Show NLTK_DATA in shell + shell: bash + run: | + echo "NLTK_DATA in shell: $NLTK_DATA" + + - name: Ensure minimal NLTK data for cache + shell: bash + run: | + python -c "import os, nltk; d = os.environ['NLTK_DATA']; import pathlib; pathlib.Path(d).mkdir(parents=True, exist_ok=True); nltk.download('wordnet', download_dir=d)" + + test: + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} + needs: [pre-commit, minimal_download_test] + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] + os: [ubuntu-latest, macos-latest, windows-latest] + exclude: + - os: windows-latest + python-version: '3.14t' # scikit-learn issue on Py3.14t on Windows + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Set NLTK_DATA environment variable + shell: bash + run: echo "NLTK_DATA=${{ github.workspace }}/nltk_data" >> $GITHUB_ENV + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install --upgrade --requirement requirements-ci.txt + + - name: Ensure minimal NLTK data for cache + shell: bash + run: | + python -c "import os, nltk; d = os.environ['NLTK_DATA']; import pathlib; pathlib.Path(d).mkdir(parents=True, exist_ok=True); nltk.download('wordnet', download_dir=d)" + + - name: Show NLTK_DATA and workspace + shell: bash + run: | + echo "GITHUB_WORKSPACE is: $GITHUB_WORKSPACE" + echo "NLTK_DATA is: $NLTK_DATA" + python -c "import os; print('Python sees GITHUB_WORKSPACE:', os.environ.get('GITHUB_WORKSPACE')); print('Python sees NLTK_DATA:', os.environ.get('NLTK_DATA'))" + + - name: List contents of NLTK data dir + shell: bash + run: ls -lR "${{ github.workspace }}/nltk_data" || echo "nltk_data not found" + + - name: Cache nltk data + uses: actions/cache@v6 + id: nltk-data-cache + with: + path: ${{ github.workspace }}/nltk_data + key: nltk_data_${{ runner.os }}_v2 + + - name: Download nltk data on cache miss + if: steps.nltk-data-cache.outputs.cache-hit != 'true' + shell: bash + run: | + python -c "import os; import nltk; from pathlib import Path; path = Path(os.environ['NLTK_DATA']); path.mkdir(parents=True, exist_ok=True); nltk.download('all', download_dir=path)" + + # --- THIRD PARTY TOOLS CACHE SECTION --- + - name: Ensure third-party directory exists + run: mkdir -p "${{ env.THIRD_PARTY_DIR }}" + + - name: Cache third-party tools + uses: actions/cache@v6 + id: third-party-cache + with: + path: ${{ env.THIRD_PARTY_DIR }} + key: third_${{ runner.os }}_${{ hashFiles('tools/github_actions/third-party.sh') }}_v1 + + - name: List contents of third-party dir before download + shell: bash + run: ls -lR "${{ env.THIRD_PARTY_DIR }}" || echo "third-party dir not found" + + - name: Download third-party data on cache miss + if: steps.third-party-cache.outputs.cache-hit != 'true' + shell: bash + run: | + chmod +x ./tools/github_actions/third-party.sh + ./tools/github_actions/third-party.sh + + - name: List contents of third-party dir after download/cache + shell: bash + run: ls -lR "${{ env.THIRD_PARTY_DIR }}" || echo "third-party dir not found" + + - name: Print NLTK data search paths + shell: bash + run: python -c "import nltk; print('NLTK data search paths:', nltk.data.path)" + + - name: Run pytest + shell: bash + run: | + pytest --numprocesses auto -rsx --doctest-modules nltk diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000..338ba5e --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,15 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v6 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..174cc30 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,112 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + gate-on-ci: + name: Gate release on CI success for this tag commit + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Check that release tag points to a develop commit + run: | + git merge-base --is-ancestor "$GITHUB_SHA" origin/develop + + - name: Verify version string in documentation + run: | + # Strip leading 'v' + RAW_VERSION=${GITHUB_REF_NAME#v} + # Strip pre-release suffixes (e.g., -rc1, -beta) + VERSION=${RAW_VERSION%%-*} + echo "Checking files for base version: $VERSION" + + for file in nltk/VERSION ChangeLog web/news.rst web/conf.py; do + if grep -Fq "$VERSION" "$file"; then + echo "✅ Found $VERSION in $file" + else + echo "❌ ERROR: Version $VERSION not found in $file!" + exit 1 + fi + done + + - name: Wait for ci.yml to complete and verify success + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + workflow_file="ci.yml" + attempts=60 + delay=20 + + for _ in $(seq 1 "$attempts"); do + run=$(gh api \ + "repos/${{ github.repository }}/actions/workflows/${workflow_file}/runs?per_page=100" \ + --jq '[.workflow_runs[] | select(.head_sha == "'"$GITHUB_SHA"'")] | sort_by(.run_number) | last') + + if [ -z "$run" ] || [ "$run" = "null" ]; then + sleep "$delay" + continue + fi + + status=$(jq -r '.status // empty' <<< "$run") + conclusion=$(jq -r '.conclusion // empty' <<< "$run") + + if [ "$status" != "completed" ]; then + sleep "$delay" + continue + fi + + if [ "$conclusion" = "success" ]; then + exit 0 + fi + + run_id=$(jq -r '.id // empty' <<< "$run") + run_number=$(jq -r '.run_number // empty' <<< "$run") + run_url="https://github.com/${{ github.repository }}/actions/runs/${run_id}" + echo "CI failed with conclusion: ${conclusion:-unknown} (run #${run_number:-unknown}). See workflow run: ${run_url}" + exit 1 + done + + echo "Timed out waiting for CI to complete for $GITHUB_SHA" + exit 1 + + build-and-github-release: + needs: gate-on-ci + name: Build and create GitHub draft release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build + + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 + with: + files: dist/* + draft: true + generate_release_notes: true diff --git a/.github/workflows/retest-pr.yml b/.github/workflows/retest-pr.yml new file mode 100644 index 0000000..0ee64cc --- /dev/null +++ b/.github/workflows/retest-pr.yml @@ -0,0 +1,71 @@ +name: Retest PR + +on: + issue_comment: + types: [created] + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + retest: + if: github.event.issue.pull_request && contains(github.event.comment.body, 'retest') + runs-on: ubuntu-latest + permissions: + actions: write + issues: write + pull-requests: read + steps: + - name: Check authorization and trigger phrase + uses: actions/github-script@v9 + with: + script: | + const assoc = context.payload.comment.author_association; + if (!['OWNER', 'MEMBER', 'COLLABORATOR'].includes(assoc)) { + core.setFailed('Not authorized (association: ' + assoc + ')'); + return; + } + const body = context.payload.comment.body; + if (!body.includes('/retest') && + !body.includes('[CI: retest]') && + !body.includes('[ci: retest]')) { + core.setFailed('No recognized retest trigger found.'); + } + + - name: Get PR head SHA + id: pr + uses: actions/github-script@v9 + with: + script: | + const pr = (await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + })).data; + core.setOutput('sha', pr.head.sha); + + - name: Re-run CI + uses: actions/github-script@v9 + with: + script: | + const runs = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'ci.yml', + head_sha: '${{ steps.pr.outputs.sha }}', + per_page: 1, + }); + if (runs.data.workflow_runs.length === 0) { + core.setFailed('No CI run found for this PR head SHA.'); + return; + } + const run = runs.data.workflow_runs[0]; + if (run.status === 'completed') { + await github.rest.actions.reRunWorkflow({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: run.id, + }); + } else { + core.info('CI is already running (status: ' + run.status + '). Nothing to re-run.'); + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f210ab6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,71 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*.class +*.jar + +*.egg +build/ +dist/ +nltk.egg-info/ +web/_build + +# Test artifacts and coverage reports +*.tox +*.errs +.hypothesis +.noseids +.coverage* +nltk/test/*.html +nltk/test/tweets* +model.crf.tagger +brown.embedding +pylintoutput +nosetests.xml +nosetests_scrubbed.xml +coverage.xml + +# editor temporary files +*.*.sw[op] +.idea +*~ + +# git mergetools backups +*.orig + +# emacs backups +*# + +# spell-check backups +*.bak + +# automatically built files for website +web/api/*.rst +web/howto/*.rst + +# iPython notebooks +.ipynb_checkpoints + +# pyenv files +.python-version + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +#vscode +.vscode/ + +# Direnv +.envrc + +# Mypy +.mypy_cache + +# macOS +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..622a7e5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: fix-byte-order-marker + - id: trailing-whitespace + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: check-yaml + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: ["--py310-plus"] + - repo: https://github.com/psf/black + rev: 25.11.0 + hooks: + - id: black + - repo: https://github.com/pycqa/isort + rev: 6.1.0 + hooks: + - id: isort + args: ['--filter-files'] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.6 + hooks: + - id: ruff + args: [--fix] diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..60ab2de --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,334 @@ +# Natural Language Toolkit (NLTK) Authors + +## Original Authors + +- Steven Bird +- Edward Loper +- Ewan Klein + +## Contributors + +- Tom Aarsen +- Rami Al-Rfou' +- Mark Amery +- Greg Aumann +- Ivan Barria +- Ingolf Becker +- Yonatan Becker +- Paul Bedaride +- Steven Bethard +- Robert Berwick +- Dan Blanchard +- Nathan Bodenstab +- Alexander Böhm +- Francis Bond +- Paul Bone +- Jordan Boyd-Graber +- Daniel Blanchard +- Phil Blunsom +- Lars Buitinck +- Cristian Capdevila +- Steve Cassidy +- Chen-Fu Chiang +- Dmitry Chichkov +- Jinyoung Choi +- Andrew Clausen +- Lucas Champollion +- Graham Christensen +- Trevor Cohn +- David Coles +- Tom Conroy +- Claude Coulombe +- Lucas Cooper +- Robin Cooper +- Chris Crowner +- James Curran +- Arthur Darcet +- Dariel Dato-on +- Selina Dennis +- Leon Derczynski +- Alexis Dimitriadis +- Nikhil Dinesh +- Liang Dong +- David Doukhan +- Rebecca Dridan +- Pablo Duboue +- Long Duong +- Christian Federmann +- Campion Fellin +- Michelle Fullwood +- Dan Garrette +- Maciej Gawinecki +- Jean Mark Gawron +- Sumukh Ghodke +- Yoav Goldberg +- Michael Wayne Goodman +- Dougal Graham +- Brent Gray +- Simon Greenhill +- Clark Grubb +- Eduardo Pereira Habkost +- Masato Hagiwara +- Lauri Hallila +- Michael Hansen +- Yurie Hara +- Will Hardy +- Tyler Hartley +- Peter Hawkins +- Saimadhav Heblikar +- Fredrik Hedman +- Helder +- Michael Heilman +- Ofer Helman +- Christopher Hench +- Bruce Hill +- Amy Holland +- Kristy Hollingshead +- Marcus Huderle +- Baden Hughes +- Nancy Ide +- Rebecca Ingram +- Edward Ivanovic +- Thomas Jakobsen +- Nick Johnson +- Eric Kafe +- Piotr Kasprzyk +- Angelos Katharopoulos +- Sudharshan Kaushik +- Chris Koenig +- Mikhail Korobov +- Denis Krusko +- Ilia Kurenkov +- Stefano Lattarini +- Pierre-François Laquerre +- Stefano Lattarini +- Haejoong Lee +- Jackson Lee +- Max Leonov +- Chris Liechti +- Hyuckin David Lim +- Tom Lippincott +- Peter Ljunglöf +- Alex Louden +- David Lukeš +- Joseph Lynch +- Nitin Madnani +- Felipe Madrigal +- Bjørn Mæland +- Dean Malmgren +- Christopher Maloof +- Rob Malouf +- Iker Manterola +- Carl de Marcken +- Mitch Marcus +- Torsten Marek +- Robert Marshall +- Marius Mather +- Duncan McGreggor +- David McClosky +- Xinfan Meng +- Dmitrijs Milajevs +- Matt Miller +- Margaret Mitchell +- Tomonori Nagano +- Jason Narad +- Shari A’aidil Nasruddin +- Lance Nathan +- Morten Neergaard +- David Nemeskey +- Eric Nichols +- Joel Nothman +- Alireza Nourian +- Alexander Oleynikov +- Pierpaolo Pantone +- Ted Pedersen +- Jacob Perkins +- Alberto Planas +- Ondrej Platek +- Alessandro Presta +- Qi Liu +- Martin Thorsen Ranang +- Michael Recachinas +- Brandon Rhodes +- Joshua Ritterman +- Will Roberts +- Stuart Robinson +- Carlos Rodriguez +- Lorenzo Rubio +- Alex Rudnick +- Jussi Salmela +- Geoffrey Sampson +- Kepa Sarasola +- Kevin Scannell +- Nathan Schneider +- Rico Sennrich +- Thomas Skardal +- Eric Smith +- Lynn Soe +- Rob Speer +- Peter Spiller +- Richard Sproat +- Ceri Stagg +- Peter Stahl +- Oliver Steele +- Thomas Stieglmaier +- Jan Strunk +- Liling Tan +- Claire Taylor +- Louis Tiao +- Steven Tomcavage +- Tiago Tresoldi +- Marcus Uneson +- Yu Usami +- Petro Verkhogliad +- Peter Wang +- Zhe Wang +- Charlotte Wilson +- Chuck Wooters +- Steven Xu +- Beracah Yankama +- Lei Ye (叶磊) +- Patrick Ye +- Geraldine Sim Wei Ying +- Jason Yoder +- Thomas Zieglier +- 0ssifrage +- ducki13 +- kiwipi +- lade +- isnowfy +- onesandzeros +- pquentin +- wvanlint +- Álvaro Justen +- bjut-hz +- Sergio Oller +- Izam Mohammed +- Will Monroe +- Elijah Rippeth +- Emil Manukyan +- Casper Lehmann-Strøm +- Andrew Giel +- Tanin Na Nakorn +- Linghao Zhang +- Colin Carroll +- Heguang Miao +- Hannah Aizenman (story645) +- George Berry +- Adam Nelson +- J Richard Snape +- Alex Constantin +- Tsolak Ghukasyan +- Prasasto Adi +- Safwan Kamarrudin +- Arthur Tilley +- Vilhjalmur Thorsteinsson +- Jaehoon Hwang +- Chintan Shah +- sbagan +- Zicheng Xu +- Albert Au Yeung +- Shenjian Zhao +- Deng Wang +- Ali Abdullah +- Stoytcho Stoytchev +- Lakhdar Benzahia +- Kheireddine Abainia +- Yibin Lin +- Artiem Krinitsyn +- Björn Mattsson +- Oleg Chislov +- Pavan Gururaj Joshi +- Ethan Hill +- Vivek Lakshmanan +- Somnath Rakshit +- Anlan Du +- Pulkit Maloo +- Brandon M. Burroughs +- John Stewart +- Iaroslav Tymchenko +- Aleš Tamchyna +- Tim Gianitsos +- Philippe Partarrieu +- Andrew Owen Martin +- Adrian Ellis +- Nat Quayle Nelson +- Yanpeng Zhao +- Matan Rak +- Nick Ulle +- Uday Krishna +- Osman Zubair +- Viresh Gupta +- Ondřej Cífka +- Iris X. Zhou +- Devashish Lal +- Gerhard Kremer +- Nicolas Darr +- Hervé Nicol +- Alexandre H. T. Dias +- Daksh Shah +- Jacob Weightman +- Bonifacio de Oliveira +- Armins Bagrats Stepanjans +- Vassilis Palassopoulos +- Ram Rachum +- Or Sharir +- Denali Molitor +- Jacob Moorman +- Cory Nezin +- Matt Chaput +- Danny Sepler +- Akshita Bhagia +- Pratap Yadav +- Hiroki Teranishi +- Ruben Cartuyvels +- Dalton Pearson +- Robby Horvath +- Gavish Poddar +- Saibo Geng +- Ahmet Yildirim +- Yuta Nakamura +- Adam Hawley +- Panagiotis Simakis +- Richard Wang +- Alexandre Perez-Lebel +- Fernando Carranza +- Martin Kondratzky +- Heungson Lee +- M.K. Pawelkiewicz +- Steven Thomas Smith +- Jan Lennartz +- Tim Sockel +- Ikram Ul Haq +- Akihiro Yamazaki +- Ron Urbach +- Vivek Kalyan +- Tom Strange https://github.com/strangetom +- Vincent Peth +- Samer Masterson +- William LaCroix +- Peter de Blanc +- Jose Cols +- Christopher Smith +- scruge1 +- Ryan Mannion +- Peter Pollak +- John Winstead +- Bradley Erickson +- Volodymyr Matsko + +## Others whose work we've taken and included in NLTK, but who didn't directly contribute it: + +### Contributors to the Porter Stemmer + +- Martin Porter +- Vivake Gupta +- Barry Wilkins +- Hiranmay Ghosh +- Chris Emerson + +### Authors of snowball arabic stemmer algorithm + +- Assem Chelli +- Abdelkrim Aries +- Lakhdar Benzahia diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..ab07baf --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,47 @@ +cff-version: 1.2.0 +title: >- + Natural Language ToolKit (NLTK) +message: >- + Please cite this software using the metadata from + 'preferred-citation'. +type: software +authors: + - name: "NLTK Team" + email: "nltk.team@gmail.com" +repository-code: "https://github.com/nltk/nltk" +url: "https://www.nltk.org" +license: Apache-2.0 +keywords: + - "NLP" + - "CL" + - "natural language processing" + - "computational linguistics" + - "parsing" + - "tagging" + - "tokenizing" + - "syntax" + - "linguistics" + - "language" + - "natural language" + - "text analytics" +preferred-citation: + title: >- + Natural Language Processing with Python: Analyzing + Text with the Natural Language Toolkit + type: book + authors: + - given-names: Steven + family-names: Bird + orcid: https://orcid.org/0000-0003-3782-7733 + - given-names: Ewan + family-names: Klein + orcid: https://orcid.org/0000-0002-0520-8447 + - given-names: Edward + family-names: Loper + year: 2009 + month: 6 + url: "https://www.nltk.org/book/" + isbn: "9780596516499" + publisher: + name: "O'Reilly Media, Inc." + website: "https://www.oreilly.com/" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8baf3c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,304 @@ +# Contributing to NLTK + +Hi! Thanks for your interest in contributing to [NLTK](https://www.nltk.org/). +:-) You'll be joining a [long list of contributors](https://github.com/nltk/nltk/blob/develop/AUTHORS.md). +In this document, we'll try to summarize everything that you need to know to +do a good job. + +## Maintenance Mode + +NLTK is in maintenance mode. We welcome bugfixes. +We can consider _minor_ enhancements if they are clearly documented in an [NLTK issue](https://github.com/nltk/nltk/issues) +and are supported by a [team member](https://github.com/orgs/nltk/teams/team-nltk) who is willing to review a PR. +(You are welcome to make a case for a _major_ enhancement, but please note we have limited capacity to deal with it. +Please enlist an NLTK team member before doing substantial coding.) + +## Code and Issues + +We use [GitHub](https://www.github.com/) to host our code repositories and +issues. The [NLTK organization on GitHub](https://github.com/nltk) has many +repositories, so we can manage better the issues and development. The most +important are: + +- [nltk/nltk](https://github.com/nltk/nltk/), the main repository with code + related to the library; +- [nltk/nltk_data](https://github.com/nltk/nltk_data), repository with data + related to corpora, taggers and other useful data that are not shipped by + default with the library, which can be downloaded by `nltk.downloader`; +- [nltk/nltk.github.com](https://github.com/nltk/nltk.github.com), NLTK website + with information about the library, documentation, link for downloading NLTK + Book etc.; +- [nltk/nltk_book](https://github.com/nltk/nltk_book), source code for the NLTK + Book. + +## Development priorities + +NLTK consists of the functionality that the Python/NLP community is motivated to contribute. +Some priority areas for development are listed in the [NLTK Wiki](https://github.com/nltk/nltk/wiki#development). + +## Git and our Branching model + +### Git + +We use [Git](https://git-scm.com/) as our [version control +system](https://en.wikipedia.org/wiki/Revision_control), so the best way to +contribute is to learn how to use it and put your changes on a Git repository. +There's plenty of documentation about Git -- you can start with the [Pro Git +book](https://git-scm.com/book/). + + +### Setting up a Development Environment + +To set up your local development environment for contributing to the main +repository [nltk/nltk](https://github.com/nltk/nltk/): + +- Fork the [nltk/nltk](https://github.com/nltk/nltk/) repository on GitHub + to your account; +- Clone your forked repository locally + (`git clone https://github.com//nltk.git`); +- Run `cd nltk` to get to the root directory of the `nltk` code base; +- Create and activate a virtual environment: + ```bash + python -m venv venv + source venv/bin/activate # On Windows: venv\Scripts\activate + ``` +- Install NLTK in editable mode with dependencies: + ```bash + pip install -e . + pip install -r pip-req.txt + ``` +- Install the pre-commit hooks: + ```bash + pip install pre-commit + pre-commit install + ``` +- Install the code formatters and linter used by the pre-commit hooks: + ```bash + pip install black isort ruff pyupgrade + ``` +- Download the datasets for running tests + (`python -m nltk.downloader all`); +- Create a remote link from your local repository to the + upstream `nltk/nltk` on GitHub + (`git remote add upstream https://github.com/nltk/nltk.git`) -- + you will need to use this `upstream` link when updating your local repository + with all the latest contributions. + +### Pre-commit hooks + +NLTK uses [pre-commit](https://pre-commit.com) to run code quality checks +before each commit. The hooks are configured in +[`.pre-commit-config.yaml`](https://github.com/nltk/nltk/blob/develop/.pre-commit-config.yaml) +and include: + +- [pre-commit-hooks](https://github.com/pre-commit/pre-commit-hooks) -- trailing whitespace, end-of-file fixer, YAML check +- [pyupgrade](https://github.com/asottile/pyupgrade) -- upgrade syntax to Python 3.10+ +- [black](https://github.com/psf/black) -- code formatting +- [isort](https://github.com/pycqa/isort) -- import sorting +- [ruff](https://github.com/astral-sh/ruff-pre-commit) -- fast Python linter with auto-fix + +You can run all hooks manually with: +```bash +pre-commit run --all-files +``` + +Or run the tools individually: +```bash +isort nltk/path/to/file.py +black nltk/path/to/file.py +ruff check nltk/path/to/file.py +``` + +### GitHub Pull requests + +We use [gitflow](https://nvie.com/posts/a-successful-git-branching-model/) to manage our branches. + +Summary of our git branching model: +- Go to the `develop` branch (`git checkout develop`); +- Get all the latest work from the upstream `nltk/nltk` repository + (`git pull upstream develop`); +- Create a new branch off of `develop` with a descriptive name (for example: + `feature/portuguese-sentiment-analysis`, `hotfix/bug-on-downloader`). You can + do it by switching to the `develop` branch (`git checkout develop`) and then + creating a new branch (`git checkout -b name-of-the-new-branch`); +- Do many small commits on that branch locally (`git add files-changed`, + `git commit -m "Add some change"`); +- Run the tests to make sure nothing breaks + (`pytest nltk/test` or `tox -e py313` if you are on Python 3.13); +- Add your name to the `AUTHORS.md` file as a contributor; +- Push to your fork on GitHub (with the name as your local branch: + `git push origin branch-name`); +- Create a pull request using the GitHub Web interface (asking us to pull the + changes from your new branch and add to them our `develop` branch); +- Wait for comments. + + +### Tips + +- Write [helpful commit + messages](https://robots.thoughtbot.com/5-useful-tips-for-a-better-commit-message). +- Anything in the `develop` branch should be deployable (no failing tests). +- Never use `git add .`: it can add unwanted files; +- Avoid using `git commit -a` unless you know what you're doing; +- Check every change with `git diff` before adding them to the index (stage + area) and with `git diff --cached` before committing; +- Make sure you add your name to our [list of contributors](https://github.com/nltk/nltk/blob/develop/AUTHORS.md); +- If you have push access to the main repository, please do not commit directly + to `develop`: your access should be used only to accept pull requests; if you + want to make a new feature, you should use the same process as other + developers so your code will be reviewed. +- See [RELEASE-HOWTO.txt](RELEASE-HOWTO.txt) to see everything you + need before creating a new NLTK release. + + +## Code Guidelines + +- Use [PEP8](https://www.python.org/dev/peps/pep-0008/); +- Write tests for your new features (please see "Tests" topic below); +- Always remember that [commented code is dead + code](https://blog.codinghorror.com/coding-without-comments/); +- Name identifiers (variables, classes, functions, module names) with readable + names (`x` is always wrong); +- When manipulating strings, we prefer either [f-string + formatting](https://docs.python.org/3/tutorial/inputoutput.html#formatted-string-literals) + (f`'{a} = {b}'`) or [new-style + formatting](https://docs.python.org/library/string.html#format-string-syntax) + (`'{} = {}'.format(a, b)`), instead of the old-style formatting (`'%s = %s' % (a, b)`); +- All `#TODO` comments should be turned into issues (use our + [GitHub issue system](https://github.com/nltk/nltk/issues)); +- Run all tests before pushing (just execute `tox`) so you will know if your + changes broke something; + +See also our [developer's +guide](https://github.com/nltk/nltk/wiki/Developers-Guide). + + +## Tests + +You should write tests for every feature you add or bug you solve in the code. +Having automated tests for every line of our code lets us make big changes +without worries: there will always be tests to verify if the changes introduced +bugs or lack of features. If we don't have tests we will be blind and every +change will come with some fear of possibly breaking something. + +For a better design of your code, we recommend using a technique called +[test-driven development](https://en.wikipedia.org/wiki/Test-driven_development), +where you write your tests **before** writing the actual code that implements +the desired feature. + +You can use `pytest` to run your tests, no matter which type of test it is: + +```bash +cd nltk/test +pytest util.doctest # doctest +pytest unit/translate/test_nist.py # unittest +pytest # all tests +``` + +If your PR only touches a single module, you can run just the relevant test +file directly with `python -m unittest` without needing pytest: + +```bash +# Run a specific test file +python -m unittest nltk.test.unit.test_tokenize + +# Run a specific test class +python -m unittest nltk.test.unit.test_tokenize.TestTreebankWordDetokenizer + +# Run a specific test method +python -m unittest nltk.test.unit.test_tokenize.TestTreebankWordDetokenizer.test_contractions +``` + +If your PR touches a module that has doctests (inline `>>>` examples in +docstrings), you can run just those doctests with `python -m doctest`: + +```bash +# Run doctests for a single module +python -m doctest nltk/metrics/distance.py + +# Run with verbose output to see each test +python -m doctest -v nltk/metrics/distance.py + +# Run a specific doctest file from the test suite +python -m doctest nltk/test/tokenize.doctest +``` + +These are faster than running the full test suite and useful for quick +iteration during development. + + +## Continuous Integration + +NLTK uses [GitHub Actions](https://github.com/nltk/nltk/actions) for continuous integration. +See [here](https://docs.github.com/en/actions) for GitHub's documentation. + +The [`.github/workflows/ci.yml`](https://github.com/nltk/nltk/blob/develop/.github/workflows/ci.yml) file configures the CI: + + - `on:` section + - ensures that this CI is run on code pushes, pull request, or through the GitHub website via `workflow_dispatch`. + + - The `pre-commit` job + - performs these steps: + - Downloads the `nltk` source code. + - Runs pre-commit on all files in the repository (black, isort, ruff, pyupgrade). + - Fails if any hooks performed a change. + + - The `minimal_download_test` job + - verifies that `nltk.download()` works on all platforms (ubuntu, macos, windows). + + - The `test` job + - tests against supported Python versions (`3.10`, `3.11`, `3.12`, `3.13`, `3.14`). + - tests on `ubuntu-latest`, `macos-latest`, and `windows-latest`. + - performs these steps: + - Downloads the `nltk` source code. + - Sets up Python using whatever version is being checked in the current execution. + - Installs dependencies via `pip install -r pip-req.txt`. + - Downloads `nltk_data`. + - Runs `pytest --numprocesses auto -rsx --doctest-modules nltk`. + +#### To run tests locally + +Using pytest directly: + +```bash +# Run all tests +pytest nltk/test + +# Run a specific test file +pytest nltk/test/unit/test_tokenize.py + +# Run tests in parallel +pip install pytest-xdist +pytest --numprocesses auto nltk/test +``` + +Using tox (to test against a specific Python version): + +```bash +pip install tox +tox -e py313 # for Python 3.13 +``` + + +## Supported Python Versions + +NLTK supports Python `3.10`, `3.11`, `3.12`, `3.13`, and `3.14`. +See `python_requires` in [setup.py](https://github.com/nltk/nltk/blob/develop/setup.py). + + +# Discussion + +We have three mail lists on Google Groups: + +- [nltk][nltk-announce], for announcements only; +- [nltk-users][nltk-users], for general discussion and user questions; +- [nltk-dev][nltk-dev], for people interested in NLTK development. + +Please feel free to contact us through the [nltk-dev][nltk-dev] mail list if +you have any questions or suggestions. Every contribution is very welcome! + +Happy hacking! (; + +[nltk-announce]: https://groups.google.com/forum/#!forum/nltk +[nltk-dev]: https://groups.google.com/forum/#!forum/nltk-dev +[nltk-users]: https://groups.google.com/forum/#!forum/nltk-users diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..d16be67 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,1586 @@ +Version 3.10.0 2026-06-11 + +* Enforce the stricter `nltk.pathsec` security policy by default +* Document the new security model and migration guidance +* Harden resource loading against path traversal and SSRF/DNS-rebinding +* Harden downloader path handling and block XML entity expansion +* Close remaining corpus-reader security edge cases +* Replace unsafe `exec()` usage in the utility CLI +* Warn on unpickling user-provided pickles +* Add HuggingFace datasets integration (`nltk.huggingface`) +* Align TnT with Brants (2000) specifications +* Fix PorterStemmer irregular-form lowercasing in NLTK mode +* Fix TransitionParser sparse index dtype for scikit-learn 1.9 +* Fix TextCat tie handling +* Fix WordNet object comparisons for incompatible types +* Cache WordNet max depth lazily for `lch_similarity()` +* Fix CCG variable direction, substitution, and type-raising bugs +* Fix Jaro similarity for single-character and empty-string cases +* Improve CI and release-maintenance workflows + +Thanks to the following contributors to 3.10.0: +13rac1, alvations, bowiechen, devesh-2002, ekaf, elias-ba, +haosenwang1018, HyperPS, ihitamandal, jancallewaert, jhnwnstd, +JuanIMartinezB, Lemm1, LinZiyuu, Mr-Neutr0n, PastelStorm, +scruge1, Syzygy2048, ylwango613, yzhaoinuw + +Version 3.9.4 2026-03-24 + +* Support Python 3.14 +* Fix bug in Levenshtein distance when substitution_cost > 2 +* Fix bug in Treebank detokeniser re quote ordering +* Fix bug in Jaro similarity for empty strings +* Several security enhancements +* Fix GHSA-rf74-v2fm-23pw: unbounded recursion in JSONTaggedDecoder +* Implement TextTiling vocabulary introduction method (Hearst 1997) +* Fix ALINE feature matrix errors and add comprehensive tests +* Support multiple VerbNet versions, fix longid/shortid regex for VerbNet ids +* Let downloader fallback to md5 when sha256 is unavailable +* Several other minor bugfixes and code cleanups + +Thanks to the following contributors to 3.9.4: +Min-Yen Kan, Eric Kafe, Emily Voss, bowiechen, Hrudhai01, +jancallewaert, Mr-Neutr0n, pollak.peter89, ylwango613, + +Version 3.9.3 2026-02-21 + +* Fix CVE-2025-14009: secure ZIP extraction in nltk.downloader (#3468) +* Block path traversal/arbitrary reads in nltk.data for protocol-less refs (#3467) +* Block path traversal/abs paths in corpus readers and FS pointers (#3479, #3480) +* Validate external StanfordSegmenter JARs using SHA256 (#3477) +* Add optional sandbox enforcement for filestring() (#3485) +* Maintenance: downloader/zipped models, CI/tooling updates + +Thanks to the following contributors to 3.9.3: +Chris Clauss, Eric Kafe, HyperPS, purificant, Shivansh-Game, Christopher Smith + +Version 3.9.2 2025-10-01 + +* Update download checksums to use SHA256 in built index +* Fix percentage escape in new-style string formatting +* replace shortened URLs using goo.gl +* Make Wordnet interoperable with various taggers and tagged corpora +* Fix saving PerceptronTagger +* Document how to reproduce old Wordnet studies +* properly initialize Portuguese corpus reader +* support for mixed rules conversion into Chomsky Normal Form +* only import tkinter if a GUI is needed +* issue #2112 with Corenlp +* new environment variable NLTK_DOWNLOADER_FORCE_INTERACTIVE_SHELL +* Lesk defaults to most frequent sense in case of ties + +Thanks to the following contributors to 3.9.2: +Jose Cols, Peter de Blanc, GeneralPoxter, Eric Kafe, William LaCroix, Jason Liu, +Samer Masterson, Mike014, purificant, Andrew Ernest Ritz, samertm, Ikram Ul Haq, +Christopher Smith, Ryan Mannion + +Version 3.9.1 2024-08-19 +* Fixed bug that prevented wordnet from loading + +Version 3.9 2024-08-18 +* Fix security vulnerability CVE-2024-39705 (breaking change) +* Replace pickled models (punkt, chunker, taggers) by new pickle-free "_tab" packages +* No longer sort Wordnet synsets and relations (sort in calling function when required) +* Only strip the last suffix in Wordnet Morphy, thus restricting synsets() results +* Add Python 3.12 support +* Many other minor fixes + +Thanks to the following contributors to 3.8.2: +Tom Aarsen, Cat Lee Ball, Veralara Bernhard, Carlos Brandt, Konstantin Chernyshev, Michael Higgins, +Eric Kafe, Vivek Kalyan, David Lukes, Rob Malouf, purificant, Alex Rudnick, Liling Tan, Akihiro Yamazaki. + +Version 3.8.1 2023-01-02 + +* Resolve RCE vulnerability in localhost WordNet Browser (#3100) +* Remove unused tool scripts (#3099) +* Resolve XSS vulnerability in localhost WordNet Browser (#3096) +* Add Python 3.11 support (#3090) + +Thanks to the following contributors to 3.8.1: +Francis Bond, John Vandenberg, Tom Aarsen + +Version 3.8 2022-12-12 + +* Refactor dispersion plot (#3082) +* Provide type hints for LazyCorpusLoader variables (#3081) +* Throw warning when LanguageModel is initialized with incorrect vocabulary (#3080) +* Fix WordNet's all_synsets() function (#3078) +* Resolve TreebankWordDetokenizer inconsistency with end-of-string contractions (#3070) +* Support both iso639-3 codes and BCP-47 language tags (#3060) +* Avoid DeprecationWarning in Regexp tokenizer (#3055) +* Fix many doctests, add doctests to CI (#3054, #3050, #3048) +* Fix bool field not being read in VerbNet (#3044) +* Greatly improve time efficiency of SyllableTokenizer when tokenizing numbers (#3042) +* Fix encodings of Polish udhr corpus reader (#3038) +* Allow TweetTokenizer to tokenize emoji flag sequences (#3034) +* Prevent LazyModule from increasing the size of nltk.__dict__ (#3033) +* Fix CoreNLPServer non-default port issue (#3031) +* Add "acion" suffix to the Spanish SnowballStemmer (#3030) +* Allow loading WordNet without OMW (#3026) +* Use input() in nltk.chat.chatbot() for Jupyter support (#3022) +* Fix edit_distance_align() in distance.py (#3017) +* Tackle performance and accuracy regression of sentence tokenizer since NLTK 3.6.6 (#3014) +* Add the Iota operator to semantic logic (#3010) +* Resolve critical errors in WordNet app (#3008) +* Resolve critical error in CHILDES Corpus (#2998) +* Make WordNet information_content() accept adjective satellites (#2995) +* Add "strict=True" parameter to CoreNLP (#2993, #3043) +* Resolve issue with WordNet's synset_from_sense_key (#2988) +* Handle WordNet synsets that were lost in mapping (#2985) +* Resolve TypeError in Boxer (#2979) +* Add function to retrieve WordNet synonyms (#2978) +* Warn about nonexistent OMW offsets instead of raising an error (#2974) +* Fix missing ic argument in res, jcn and lin similarity functions of WordNet (#2970) +* Add support for the extended OMW (#2946) +* Fix LC cutoff policy of text tiling (#2936) +* Optimize ConditionalFreqDist.__add__ performance (#2939) +* Add Markdown corpus reader (#2902) + +Thanks to the following contributors to 3.8: +Alexandre Perez-Lebel, David Lukes, Eric Kafe, Fernando Carranza, Heungson Lee, +Hoyeol Kim, James Huang, Jelle Zijlstra, Louis-Justin Tallot, M.K. Pawelkiewicz, +Jan Lennartz, Malinda Dilhara, Martin Kondratzky, Rob Malouf, Saud Kadiri, +Siddhesh Mhadnak, Stephan Hasler, Steve Smith, Tom Aarsen, Tyler Sheaffer, +Yue Zhao, cestwc, elespike, purificant, richardyy1188 + +Version 3.7 2022-02-09 + +* Improve and update the NLTK team page on nltk.org (#2855, #2941) +* Drop support for Python 3.6, support Python 3.10 (#2920) + +Thanks to the following contributors to 3.7: +Tom Aarsen + +Version 3.6.7 2021-12-28 + +* Resolve IndexError in `sent_tokenize` and `word_tokenize` (#2922) + +Thanks to the following contributors to 3.6.7: +Tom Aarsen + +Version 3.6.6 2021-12-21 + +* Refactor `gensim.doctest` to work for gensim 4.0.0 and up (#2914) +* Add Precision, Recall, F-measure, Confusion Matrix to Taggers (#2862) +* Added warnings if .zip files exist without any corresponding .csv files. (#2908) +* Fix `FileNotFoundError` when the `download_dir` is a non-existing nested folder (#2910) +* Rename omw to omw-1.4 (#2907) +* Resolve ReDoS opportunity by fixing incorrectly specified regex (#2906) +* Support OMW 1.4 (#2899) +* Deprecate Tree get and set node methods (#2900) +* Fix broken inaugural test case (#2903) +* Use Multilingual Wordnet Data from OMW with newer Wordnet versions (#2889) +* Keep NLTKs "tokenize" module working with pathlib (#2896) +* Make prettyprinter to be more readable (#2893) +* Update links to the nltk book (#2895) +* Add `CITATION.cff` to nltk (#2880) +* Resolve serious ReDoS in PunktSentenceTokenizer (#2869) +* Delete old CI config files (#2881) +* Improve Tokenize documentation + add TokenizerI as superclass for TweetTokenizer (#2878) +* Fix expected value for BLEU score doctest after changes from #2572 +* Add multi Bleu functionality and tests (#2793) +* Deprecate 'return_str' parameter in NLTKWordTokenizer and TreebankWordTokenizer (#2883) +* Allow empty string in CFG's + more (#2888) +* Partition `tree.py` module into `tree` package + pickle fix (#2863) +* Fix several TreebankWordTokenizer and NLTKWordTokenizer bugs (#2877) +* Rewind Wordnet data file after each lookup (#2868) +* Correct __init__ call for SyntaxCorpusReader subclasses (#2872) +* Documentation fixes (#2873) +* Fix levenstein distance for duplicated letters (#2849) +* Support alternative Wordnet versions (#2860) +* Remove hundreds of formatting warnings for nltk.org (#2859) +* Modernize `nltk.org/howto` pages (#2856) +* Fix Bleu Score smoothing function from taking log(0) (#2839) +* Update third party tools to newer versions and removing MaltParser fixed version (#2832) +* Fix TypeError: _pretty() takes 1 positional argument but 2 were given in sem/drt.py (#2854) +* Replace `http` with `https` in most URLs (#2852) + +Thanks to the following contributors to 3.6.6: +Adam Hawley, BatMrE, Danny Sepler, Eric Kafe, Gavish Poddar, Panagiotis Simakis, +RnDevelover, Robby Horvath, Tom Aarsen, Yuta Nakamura, Mohaned Mashaly + +Version 3.6.5 2021-10-11 + +* modernised nltk.org website +* addressed LGTM.com issues +* support ZWJ sequences emoji and skin tone modifer emoji in TweetTokenizer +* METEOR evaluation now requires pre-tokenized input +* Code linting and type hinting +* implement get_refs function for DrtLambdaExpression +* Enable automated CoreNLP, Senna, Prover9/Mace4, Megam, MaltParser CI tests +* specify minimum regex version that supports regex.Pattern +* avoid re.Pattern and regex.Pattern which fail for Python 3.6, 3.7 + +Thanks to the following contributors to 3.6.5: +Tom Aarsen, Saibo Geng, Mohaned Mashaly, Dimitri Papadopoulos, Danny Sepler, +Ahmet Yildirim, RnDevelover, yutanakamura + +Version 3.6.4 2021-10-01 + +* deprecate `nltk.usage(obj)` in favor of `help(obj)` +* resolve ReDoS vulnerability in Corpus Reader +* solidify performance tests +* improve phone number recognition in tweet tokenizer +* refactored CISTEM stemmer for German +* identify NLTK Team as the author +* replace travis badge with github actions badge +* add SECURITY.md + +Thanks to the following contributors to 3.6.4: +Tom Aarsen, Mohaned Mashaly, Dimitri Papadopoulos Orfanos, purificant, Danny Sepler + +Version 3.6.3 2021-09-19 +* Dropped support for Python 3.5 +* Run CI tests on Windows, too +* Moved from Travis CI to GitHub Actions +* Code and comment cleanups +* Visualize WordNet relation graphs using Graphviz +* Fixed large error in METEOR score +* Apply isort, pyupgrade, black, added as pre-commit hooks +* Prevent debug_decisions in Punkt from throwing IndexError +* Resolved ZeroDivisionError in RIBES with dissimilar sentences +* Initialize WordNet IC total counts with smoothing value +* Fixed AttributeError for Arabic ARLSTem2 stemmer +* Many fixes and improvements to lm language model package +* Fix bug in nltk.metrics.aline, C_skip = -10 +* Improvements to TweetTokenizer +* Optional show arg for FreqDist.plot, ConditionalFreqDist.plot +* edit_distance now computes Damerau-Levenshtein edit-distance + +Thanks to the following contributors to 3.6.3: +Tom Aarsen, Abhijnan Bajpai, Michael Wayne Goodman, Michał Górny, Maarten ter Huurne, +Manu Joseph, Eric Kafe, Ilia Kurenkov, Daniel Loney, Rob Malouf, Mohaned Mashaly, +purificant, Danny Sepler, Anthony Sottile + +Version 3.6.2 2021-04-20 +* move test code to nltk/test +* clean up some doctests +* fix bug in NgramAssocMeasures (order preserving fix) +* fixes for compatibility with Pypy 7.3.4 + +Thanks to the following contributors to 3.6.2: +Ruben Cartuyvels, Rob Malouf, Dalton Pearson, Danny Sepler + +Version 3.6 2021-04-07 +* add support for Python 3.9 +* add Tree.fromlist +* compute Minimum Spanning Tree of unweighted graph using BFS +* fix bug with infinite loop in Wordnet closure and tree +* fix bug in calculating BLEU using smoothing method 4 +* Wordnet synset similarities work for all pos +* new Arabic light stemmer (ARLSTem2) +* new syllable tokenizer (LegalitySyllableTokenizer) +* remove nose in favor of pytest +* misc bug fixes, code cleanups, test cleanups, efficiency improvements + +Thanks to the following contributors to 3.6: +Tom Aarsen, K Abainia, Akshita Bhagia, Andrew Bird, Thomas Bird, +Tom Conroy, Christopher Hench, Andrew Jorgensen, Eric Kafe, +Ilia Kurenkov, Yeting Li, Joseph Manu, Marius Mather, Denali Molitor, +Jacob Moorman, Philippe Ombredanne, Vassilis Palassopoulos, Ram Rachum, +Danny Sepler, Or Sharir, Brad Solomon, Hiroki Teranishi, Constantin Weisser, +Pratap Yadav, Louis Yang + +Version 3.5 2020-04-13 +* add support for Python 3.8 +* drop support for Python 2 +* create NLTK's own Tokenizer class distinct from the Treebank reference tokeniser +* update Vader sentiment analyser +* fix JSON serialization of some PoS taggers +* minor improvements in grammar.CFG, Vader, pl196x corpus reader, StringTokenizer +* change implementation <= and >= for FreqDist so they are partial orders +* make FreqDist iterable +* correctly handle Penn Treebank trees with a unlabeled branching top node. + +Thanks to the following contributors to 3.5: +Nicolas Darr, Gerhard Kremer, Liling Tan, Christopher Hench, Alexandre Dias, Hervé Nicol, +Pierpaolo Pantone, Bonifacio de Oliveira, Maciej Gawinecki, BLKSerene, hoefling, alvations, +pyfisch, srhrshr + +Version 3.4.5 2019-08-20 +* Fixed security bug in downloader: Zip slip vulnerability - for the unlikely + situation where a user configures their downloader to use a compromised server + https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14751) + +Thanks to the following contributors to 3.4.5: +Mike Salvatore + +Version 3.4.4 2019-07-04 +* fix bug in plot function (probability.py) +* add improved PanLex Swadesh corpus reader + +Thanks to the following contributors to 3.4.4: +Devashish Lal, Liling Tan + +Version 3.4.3 2019-06-07 + +* add Text.generate() +* add QuadgramAssocMeasures +* add SSP to tokenizers +* return confidence of best tag from AveragedPerceptron +* make plot methods return Axes objects +* don't require list arguments to PositiveNaiveBayesClassifier.train +* fix Tree classes to work with native Python copy library +* fix inconsistency for NomBank +* fix random seeding in LanguageModel.generate +* fix ConditionalFreqDist mutation on tabulate/plot call +* fix broken links in documentation +* fix misc Wordnet issues +* update installation instructions + +Thanks to the following contributors to 3.4.3: +alvations, Bharat123rox, cifkao, drewmiller, free-variation, henchc +irisxzhou, nick-ulle, ppartarr, simonepri, yigitsever, zhaoyanpeng + +Version 3.4.1 2019-04-17 + +* add chomsky_normal_form for CFGs +* add meteor score +* add minimum edit/Levenshtein distance based alignment function +* allow access to collocation list via text.collocation_list() +* support corenlp server options +* drop support for Python 3.4 +* other minor fixes + +Thanks to the following contributors to 3.4.1: +Adrian Ellis, Andrew Martin, Ayush Kaushal, BLKSerene, Bharat +Raghunathan, Franklin Chen, KMiNT21 Kevin Brown, Liling Tan, +Matan Rak, Nat Quayle Nelson, Osman Zubair, Purificant, +Uday Krishna, Viresh Gupta + +Version 3.4 2018-11-17 +* Support Python 3.7 +* Language Modeling incl Kneser-Ney, Witten-Bell, Good-Turing +* Cistem Stemmer for German +* Support Russian National Corpus incl POS tag model +* Decouple sentiment and twitter packages +* Minor extensions for WordNet +* K-alpha +* Fix warning messages for corenlp +* Comprehensive code cleanups +* Many other minor fixes +* Switch continuous integration from Jenkins to Travis + +Special thanks to Ilia Kurenkov (Language Model package), Liling Tan (Python 3.7, Travis-CI), +and purificant (code cleanups). Thanks also to: Afshin Sadeghi, Ales Tamchyna, Alok Debnath, +aquatiko, Coykto, Denis Kataev, dnc1994, Fabian Howard, Frankie Robertson, Iaroslav Tymchenko, +Jayakrishna Sahit, LBenzahia, Leonie Weißweiler, Linghao Zhang, Rohit Kumar, sahitpj, +Tim Gianitsos, vagrant, 53X + +Version 3.3 2018-05-06 +* Support Python 3.6 +* New interface to CoreNLP +* Support synset retrieval by sense key +* Minor fixes to CoNLL Corpus Reader, AlignedSent +* Fixed minor inconsistencies in APIs and API documentation +* Better conformance to PEP8 +* Drop moses.py (incompatible license) + +Special thanks to Liling Tan for leading our transition to Python 3.6. +Thanks to other contributors listed here: https://github.com/nltk/nltk/blob/develop/AUTHORS.md + +Version 3.2.5 2017-09-24 + +* Arabic stemmers (ARLSTem, Snowball) +* NIST MT evaluation metric and added NIST international_tokenize +* Moses tokenizer +* Document Russian tagger +* Fix to Stanford segmenter +* Improve treebank detokenizer, VerbNet, Vader +* Misc code and documentation cleanups +* Implement fixes suggested by LGTM + +Thanks to the following contributors to 3.2.5: +Ali Abdullah, Lakhdar Benzahia, Henry Elder, Campion Fellin, +Tsolak Ghukasyan, Thanh Ha, Jean Helie, Nelson Liu, +Nathan Schneider, Chintan Shah, Fábio Silva, Liling Tan, +Ziyao Wei, Zicheng Xu, Albert Au Yeung, AbdealiJK, +porqupine, sbagan, xprogramer + +Version 3.2.4 2017-05-21 + +* remove load-time dependency on Python requests library +* add support for Arabic in StanfordSegmenter +* fix MosesDetokenizer on irregular quote tokens + +Thanks to the following contributors to 3.2.4: +Alex Constantin, Hatem Nassrat, Liling Tan + +Version 3.2.3 2017-05-16 + +* new interface to Stanford CoreNLP Web API +* improved Lancaster stemmer with customizable rules from Whoosh +* improved Treebank tokenizer +* improved support for GLEU score +* adopt new Abstract base class style +* support custom tab files for extending WordNet +* make synset_from_pos_and_offset a public method +* make non-English WordNet lemma lookups case-insensitive +* speed up TnT tagger +* speed up FreqDist and ConditionalFreqDist +* support additional quotes in TreebankWordTokenizer +* clean up Tk's postscript output +* drop explicit support for corpora not distributed with NLTK to streamline testing +* allow iterator in perceptron tagger training +* allow for curly bracket quantifiers in chunk.regexp.CHUNK_TAG_PATTERN +* new corpus reader for MWA subset of PPDB +* improved testing framework + +Thanks to the following contributors to 3.2.3: +Mark Amery, Carl Bolz, Abdelhak Bougouffa, Matt Chaput, Michael Goodman, +Jaehoon Hwang, Naoya Kanai, Jackson Lee, Christian Meyer, Dmitrijs Milajevs, +Adam Nelson, Pierpaolo Pantone, Liling Tan, Vilhjalmur Thorsteinsson, +Arthur Tilley, jmhutch, Yorwba, eromoe and others + +Version 3.2.2 2016-12-31 +* added Kondrak's Aline algorithm +* added ChrF and GLEU MT evaluation metrics +* added Russian pos tagger model +* added Moses detokenizer +* rewrite Porter Stemmer +* rewrite FrameNet corpus reader + (adds frame parameter to fes(), lus(), exemplars() + see https://www.nltk.org/howto/framenet.html) +* updated FrameNet Corpus to version 1.7 +* fixes to stanford_segmenter.py, SentiText, CoNLL Corpus Reader +* fixes to BLEU, naivebayes, Krippendorff's alpha, Punkt +* fixes to tests for TransitionParser, Senna, edit distance +* fixes to Moses Tokenizer and Detokenizer +* improved TweetTokenizer +* strip trailing whitespace when splitting sentences +* handle inverted exclamation mark in ToktokTokenizer +* resolved some issues with Python 3.5 support +* improvements to testing framework +* clean up dependencies + +Thanks to the following contributors to 3.2.2: + +Prasasto Adi, Mark Amery, Geoff Bacon, George Berry, Colin Carroll, Alexis Dimitriadis, +Nicholas Fabina, German Ferrero, Tsolak Ghukasyan, Hyuckin David Lim, Naoya Kanai, +Greg Kondrak, Igor Korolev, Tim Leslie, Rob Malouf, Heguang Miao, Dmitrijs Milajevs, +Adam Nelson, Dennis O'Brien, Qi Liu, Pierpaolo Pantone, Andy Reagan, Mike Recachinas, +Nathan Schneider, Jānis Šlapiņš, Richard Snape, Liling Tan, Marcus Uneson, +Linghao Zhang, drevicko, SaintNazaire + +Version 3.2.1 2016-04-09 +* Support for CCG semantics, Stanford segmenter, VADER lexicon +* Fixes to BLEU score calculation, CHILDES corpus reader +* Other miscellaneous fixes + +Thanks to the following contributors to 3.2.1: +Andrew Giel, Casper Lehmann-Strøm, David Madl, Tanin Na Nakorn, +Guilherme Nardari, Philippe Ombredanne, Nathan Schneider, Liling Tan, +Josiah Wang, venticello + +Version 3.2 2016-03-03 +* Fixes for Python 3.5 +* Code cleanups now Python 2.6 is no longer supported +* Improvements to documentation +* Comprehensive use of os.path for platform-specific path handling +* Support for PanLex +* Support for third party download locations for NLTK data +* Fix bugs in IBM method 3 smoothing and BLEU calculation +* Support smoothing for BLEU score and corpus-level BLEU +* Support RIBES score +* Improvements to TweetTokenizer +* Updates for Stanford API +* Add mathematical operators to ConditionalFreqDist +* Fix bug in sentiwordnet for adjectives +* Merged internal implementations of Trie + +Thanks to the following contributors to 3.2: +Santiago Castro, Jihun Choi, Graham Christensen, Andrew Drozdov, Long +Duong, Kyriakos Georgiou, Michael Wayne Goodman, Clark Grubb, Tah Wei +Hoon, David Kamholz, Ewan Klein, Reed Loden, Rob Malouf, Philippe +Ombredanne, Josh Owen, Pierpaolo Pantone, Mike Recachinas, Elijah +Rippeth, Thomas Stieglmaier, Liling Tan, Philip Tzou, Pratap Vardhan. + +Version 3.1 2015-10-15 +* Fixes for Python 3.5 (drop support for capturing groups in regexp tokenizer) +* Drop support for Python 2.6 +* Adopt perceptron tagger for new default POS tagger nltk.pos_tag +* Stanford Neural Dependency Parser wrapper +* Sentiment analysis package incl VADER +* Improvements to twitter package +* Multi word expression tokenizer +* Support for everygram and skipgram +* consistent evaluation metric interfaces, putting reference before hypothesis +* new nltk.translate module, incorporating the old align module +* implement stack decoder +* clean up Alignment interface +* CorpusReader method to support access to license and citation +* Multext East Corpus and MTECorpusReader +* include six module to streamline installation on MS Windows + +Thanks to the following contributors to 3.1: +Le Tuan Anh, Petra Barancikova, Alexander Böhm, Francis Bond, +Long Duong, Anna Garbar, Matthew Honnibal, Tah Wei Hoon, Ewan Klein, +Rob Malouf, Dmitrijs Milajevs, Will Monroe, Sergio Oller, Pierpaolo +Pantone, Jacob Perkins, Lorenzo Rubio, Thomas Stieglmaier, Liling Tan, +Pratap Vardhan + +Version 3.0.5 2015-09-05 +* rewritten IBM models, and new IBM Model 4 and 5 implementations +* new Twitter package +* stabilized MaltParser API +* improved regex tagger +* improved documentation on contributing +* minor improvements to documentation and testing + +Thanks to the following contributors to 3.0.5: +Álvaro Justen, Dmitrijs Milajevs, Ewan Klein, Heran Lin, Justin Hammar, +Liling Tan, Long Duong, Lorenzo Rubio, Pierpaolo Pantone, Tah Wei Hoon + +Version 3.0.4 2015-07-13 +* minor bug fixes and enhancements + +Thanks to the following contributors to 3.0.4: +Nicola Bova, Santiago Castro, Len Remmerswaal, Keith Suderman, kabayan55, +pln-fing-udelar (NLP Group, Instituto de Computación, Facultad de Ingeniería, Universidad de la República, Uruguay). + +Version 3.0.3 2015-06-12 +* bug fixes (Stanford NER, Boxer, Snowball, treebank tokenizer, + dependency graph, KneserNey, BLEU) +* code clean-ups +* default POS tagger permits tagset to be specified +* gensim illustration +* tgrep implementation +* added PanLex Swadesh corpora +* visualisation for aligned bitext +* support for Google App Engine +* POSTagger renamed StanfordPOSTagger, NERTagger renamed StanfordNERTagger + +Thanks to the following contributors to 3.0.3: + +Long Duong, Pedro Fialho, Dan Garrette, Helder, Saimadhav Heblikar, +Chris Inskip, David Kamholz, Dmitrijs Milajevs, Smitha Milli, +Tom Mortimer-Jones, Avital Pekker, Jonathan Pool, Sam Raker, +Will Roberts, Dmitry Sadovnychyi, Nathan Schneider, Anirudh W + +Version 3.0.2 2015-03-13 +* make pretty-printing method names consistent +* improvements to Portuguese stemmer +* transition-based dependency parsers +* dependency graph visualisation for ipython notebook +* interfaces for Senna, BLLIP, python-crfsuite +* NKJP corpus reader +* code clean ups, minor bug fixes + +Thanks to the following contributors to 3.0.2: + +Long Duong, Saimadhav Heblikar, Helder, Mikhail Korobov, Denis Krusko, +Alex Louden, Felipe Madrigal, David McClosky, Dmitrijs Milajevs, +Ondrej Platek, Nathan Schneider, Dávid Márk Nemeskey, 0ssifrage, ducki13, kiwipi. + +Version 3.0.1 2015-01-12 +* fix setup.py for new version of setuptools + +Version 3.0.0 2014-09-07 +* minor bugfixes +* added phrase extraction code by Liling Tan and Fredrik Hedman + +Thanks to the following contributors to 3.0.0: +Mark Amery, Ivan Barria, Ingolf Becker, Francis Bond, Lars +Buitinck, Cristian Capdevila, Arthur Darcet, Michelle Fullwood, +Dan Garrette, Dougal Graham, Dan Garrette, Dougal Graham, Lauri +Hallila, Tyler Hartley, Fredrik Hedman, Ofer Helman, Bruce Hill, +Marcus Huderle, Nancy Ide, Nick Johnson, Angelos Katharopoulos, +Ewan Klein, Mikhail Korobov, Chris Liechti, Peter Ljunglof, +Joseph Lynch, Haejoong Lee, Peter Ljunglöf, Dean Malmgren, Rob +Malouf, Thorsten Marek, Dmitrijs Milajevs, Shari A’aidil +Nasruddin, Lance Nathan, Joel Nothman, Alireza Nourian, Alexander +Oleynikov, Ted Pedersen, Jacob Perkins, Will Roberts, Alex +Rudnick, Nathan Schneider, Geraldine Sim Wei Ying, Lynn Soe, +Liling Tan, Louis Tiao, Marcus Uneson, Yu Usami, Steven Xu, Zhe +Wang, Chuck Wooters, lade, isnowfy, onesandzeros, pquentin, wvanlint + +Version 3.0b2 2014-08-21 +* minor bugfixes and clean-ups +* renamed remaining parse_ methods to read_ or load_, cf issue #656 +* added Paice's method of evaluating stemming algorithms + +Thanks to the following contributors to 3.0.0b2: Lars Buitinck, +Cristian Capdevila, Lauri Hallila, Ofer Helman, Dmitrijs Milajevs, +lade, Liling Tan, Steven Xu + +Version 3.0.0b1 2014-07-11 +* Added SentiWordNet corpus and corpus reader +* Fixed support for 10-column dependency file format +* Changed Tree initialization to use fromstring + +Thanks to the following contributors to 3.0b1: Mark Amery, Ivan +Barria, Ingolf Becker, Francis Bond, Lars Buitinck, Arthur Darcet, +Michelle Fullwood, Dan Garrette, Dougal Graham, Dan Garrette, Dougal +Graham, Tyler Hartley, Ofer Helman, Bruce Hill, Marcus Huderle, Nancy +Ide, Nick Johnson, Angelos Katharopoulos, Ewan Klein, Mikhail Korobov, +Chris Liechti, Peter Ljunglof, Joseph Lynch, Haejoong Lee, Peter +Ljunglöf, Dean Malmgren, Rob Malouf, Thorsten Marek, Dmitrijs +Milajevs, Shari A’aidil Nasruddin, Lance Nathan, Joel Nothman, Alireza +Nourian, Alexander Oleynikov, Ted Pedersen, Jacob Perkins, Will +Roberts, Alex Rudnick, Nathan Schneider, Geraldine Sim Wei Ying, Lynn +Soe, Liling Tan, Louis Tiao, Marcus Uneson, Yu Usami, Steven Xu, Zhe +Wang, Chuck Wooters, isnowfy, onesandzeros, pquentin, wvanlint + +Version 3.0a4 2014-05-25 +* IBM Models 1-3, BLEU, Gale-Church aligner +* Lesk algorithm for WSD +* Open Multilingual WordNet +* New implementation of Brill Tagger +* Extend BNCCorpusReader to parse the whole BNC +* MASC Tagged Corpus and corpus reader +* Interface to Stanford Parser +* Code speed-ups and clean-ups +* API standardisation, including fromstring method for many objects +* Improved regression testing setup +* Removed PyYAML dependency + +Thanks to the following contributors to 3.0a4: +Ivan Barria, Ingolf Becker, Francis Bond, Arthur Darcet, Dan Garrette, +Ofer Helman, Dougal Graham, Nancy Ide, Ewan Klein, Mikhail Korobov, +Chris Liechti, Peter Ljunglof, Joseph Lynch, Rob Malouf, Thorsten Marek, +Dmitrijs Milajevs, Shari A’aidil Nasruddin, Lance Nathan, Joel Nothman, +Jacob Perkins, Lynn Soe, Liling Tan, Louis Tiao, Marcus Uneson, Steven Xu, +Geraldine Sim Wei Ying + +Version 3.0a3 2013-11-02 +* support for FrameNet contributed by Chuck Wooters +* support for Universal Declaration of Human Rights Corpus (udhr2) +* major API changes: + - Tree.node -> Tree.label() / Tree.set_label() + - Chunk parser: top_node -> root_label; chunk_node -> chunk_label + - WordNet properties are now access methods, e.g. Synset.definition -> Synset.definition() + - relextract: show_raw_rtuple() -> rtuple(), show_clause() -> clause() +* bugfix in texttiling +* replaced simplify_tags with support for universal tagset (simplify_tags=True -> tagset='universal') +* Punkt default behavior changed to realign sentence boundaries after trailing parenthesis and quotes +* deprecated classify.svm (use scikit-learn instead) +* various efficiency improvements + +Thanks to the following contributors to 3.0a3: +Lars Buitinck, Marcus Huderle, Nick Johnson, Dougal Graham, Ewan Klein, +Mikhail Korobov, Haejoong Lee, Peter Ljunglöf, Dean Malmgren, Lance Nathan, +Alexander Oleynikov, Nathan Schneider, Chuck Wooters, Yu Usami, Steven Xu, +pquentin, wvanlint + +Version 3.0a2 2013-07-12 +* speed improvements in word_tokenize, GAAClusterer, TnT tagger, Baum Welch, HMM tagger +* small improvements in collocation finders, probability, modelling, Porter Stemmer +* bugfix in lowest common hypernyn calculation (used in path similarity measures) +* code cleanups, docstring cleanups, demo fixes + +Thanks to the following contributors to 3.0a2: +Mark Amery, Lars Buitinck, Michelle Fullwood, Dan Garrette, Dougal Graham, +Tyler Hartley, Bruce Hill, Angelos Katharopoulos, Mikhail Korobov, +Rob Malouf, Joel Nothman, Ted Pedersen, Will Roberts, Alex Rudnick, +Steven Xu, isnowfy, onesandzeros + +Version 3.0a1 2013-02-14 +* reinstated tkinter support (Haejoong Lee) + +Version 3.0a0 2013-01-14 +* alpha release of first version to support Python 2.6, 2.7, and 3. + +Version 2.0.4 2012-11-07 +* minor bugfix (removed numpy dependency) + +Version 2.0.3 2012-09-24 + +* fixed corpus/reader/util.py to support Python 2.5 +* make MaltParser safe to use in parallel +* fixed bug in inter-annotator agreement +* updates to various doctests (nltk/test) +* minor bugfixes + +Thanks to the following contributors to 2.0.3: +Robin Cooper, Pablo Duboue, Christian Federmann, Dan Garrette, Ewan Klein, +Pierre-François Laquerre, Max Leonov, Peter Ljunglöf, Nitin Madnani, Ceri Stagg + +Version 2.0.2 2012-07-05 + +* improvements to PropBank, NomBank, and SemCor corpus readers +* interface to full Penn Treebank Corpus V3 (corpus.ptb) +* made wordnet.lemmas case-insensitive +* more flexible padding in model.ngram +* minor bugfixes and documentation enhancements +* better support for automated testing + +Thanks to the following contributors to 2.0.2: +Daniel Blanchard, Mikhail Korobov, Nitin Madnani, Duncan McGreggor, +Morten Neergaard, Nathan Schneider, Rico Sennrich. + +Version 2.0.1 2012-05-15 + +* moved NLTK to GitHub: https://github.com/nltk +* set up integration testing: https://jenkins.shiningpanda.com/nltk/ (Morten Neergaard) +* converted documentation to Sphinx format: https://www.nltk.org/api/nltk.html +* dozens of minor enhancements and bugfixes: https://github.com/nltk/nltk/commits/ +* dozens of fixes for conformance with PEP-8 +* dozens of fixes to ensure operation with Python 2.5 +* added interface to Lin's Dependency Thesaurus (Dan Blanchard) +* added interface to scikit-learn classifiers (Lars Buitinck) +* added segmentation evaluation measures (David Doukhan) + +Thanks to the following contributors to 2.0.1 (since 2.0b9, July 2010): +Rami Al-Rfou', Yonatan Becker, Steven Bethard, Daniel Blanchard, Lars +Buitinck, David Coles, Lucas Cooper, David Doukhan, Dan Garrette, +Masato Hagiwara, Michael Hansen, Michael Heilman, Rebecca Ingram, +Sudharshan Kaushik, Mikhail Korobov, Peter Ljunglof, Nitin Madnani, +Rob Malouf, Tomonori Nagano, Morten Neergaard, David Nemeskey, +Joel Nothman, Jacob Perkins, Alessandro Presta, Alex Rudnick, +Nathan Schneider, Stefano Lattarini, Peter Stahl, Jason Yoder + +Version 2.0.1 (rc1) 2011-04-11 + +NLTK: +* added interface to the Stanford POS Tagger +* updates to sem.Boxer, sem.drt.DRS +* allow unicode strings in grammars +* allow non-string features in classifiers +* modifications to HunposTagger +* issues with DRS printing +* fixed bigram collocation finder for window_size > 2 +* doctest paths no longer presume unix-style pathname separators +* fixed issue with NLTK's tokenize module colliding with the Python tokenize module +* fixed issue with stemming Unicode strings +* changed ViterbiParser.nbest_parse to parse +* ChaSen and KNBC Japanese corpus readers +* preserve case in concordance display +* fixed bug in simplification of Brown tags +* a version of IBM Model 1 as described in Koehn 2010 +* new class AlignedSent for aligned sentence data and evaluation metrics +* new nltk.util.set_proxy to allow easy configuration of HTTP proxy +* improvements to downloader user interface to catch URL and HTTP errors +* added CHILDES corpus reader +* created special exception hierarchy for Prover9 errors +* significant changes to the underlying code of the boxer interface +* path-based wordnet similarity metrics use a fake root node for verbs, following the Perl version +* added ability to handle multi-sentence discourses in Boxer +* added the 'english' Snowball stemmer +* simplifications and corrections of Earley Chart Parser rules +* several changes to the feature chart parsers for correct unification +* bugfixes: FreqDist.plot, FreqDist.max, NgramModel.entropy, CategorizedCorpusReader, DecisionTreeClassifier +* removal of Python >2.4 language features for 2.4 compatibility +* removal of deprecated functions and associated warnings +* added semantic domains to wordnet corpus reader +* changed wordnet similarity functions to include instance hyponyms +* updated to use latest version of Boxer + +Data: +* JEITA Public Morphologically Tagged Corpus (in ChaSen format) +* KNB Annotated corpus of Japanese blog posts +* Fixed some minor bugs in alvey.fcfg, and added number of parse trees in alvey_sentences.txt +* added more comtrans data + +Documentation: +* minor fixes to documentation +* NLTK Japanese book (chapter 12) by Masato Hagiwara + +NLTK-Contrib: +* Viethen and Dale referring expression algorithms + + +Version 2.0b9 2010-07-25 + +NLTK: +* many code and documentation cleanups +* Added port of Snowball stemmers +* Fixed loading of pickled tokenizers (issue 556) +* DecisionTreeClassifier now handles unknown features (issue 570) +* Added error messages to LogicParser +* Replaced max_models with end_size to prevent Mace from hanging +* Added interface to Boxer +* Added nltk.corpus.semcor to give access to SemCor 3.0 corpus (issue 530) +* Added support for integer- and float-valued features in maxent classifiers +* Permit NgramModels to be pickled +* Added Sourced Strings (see test/sourcedstring.doctest for details) +* Fixed bugs in with Good-Turing and Simple Good-Turing Estimation (issue 26) +* Add support for span tokenization, aka standoff annotation of segmentation (incl Punkt) +* allow unicode nodes in Tree.productions() +* Fixed WordNet's morphy to be consistent with the original implementation, + taking the shortest returned form instead of an arbitrary one (issues 427, 487) +* Fixed bug in MaxentClassifier +* Accepted bugfixes for YCOE corpus reader (issue 435) +* Added test to _cumulative_frequencies() to correctly handle the case when no arguments are supplied +* Added a TaggerI interface to the HunPos open-source tagger +* Return 0, not None, when no count is present for a lemma in WordNet +* fixed pretty-printing of unicode leaves +* More efficient calculation of the leftcorner relation for left corner parsers +* Added two functions for graph calculations: transitive closure and inversion. +* FreqDist.pop() and FreqDist.popitems() now invalidate the caches (issue 511) + +Data: +* Added SemCor 3.0 corpus (Brown Corpus tagged with WordNet synsets) +* Added LanguageID corpus (trigram counts for 451 languages) +* Added grammar for a^n b^n c^n + +NLTK-Contrib: +* minor updates + +Thanks to the following contributors to 2.0b9: + +Steven Bethard, Francis Bond, Dmitry Chichkov, Liang Dong, Dan Garrette, +Simon Greenhill, Bjorn Maeland, Rob Malouf, Joel Nothman, Jacob Perkins, +Alberto Planas, Alex Rudnick, Geoffrey Sampson, Kevin Scannell, Richard Sproat + + +Version 2.0b8 2010-02-05 + +NLTK: +* fixed copyright and license statements +* removed PyYAML, and added dependency to installers and download instructions +* updated to LogicParser, DRT (Dan Garrette) +* WordNet similarity metrics return None instead of -1 when + they fail to find a path (Steve Bethard) +* shortest_path_distance uses instance hypernyms (Jordan Boyd-Graber) +* clean_html improved (Bjorn Maeland) +* batch_parse, batch_interpret and batch_evaluate functions allow + grammar or grammar filename as argument +* more Portuguese examples (portuguese_en.doctest, examples/pt.py) + +NLTK-Contrib: +* Aligner implementations (Christopher Crowner, Torsten Marek) +* ScriptTranscriber package (Richard Sproat and Kristy Hollingshead) + +Book: +* updates for second printing, correcting errata + https://nltk.googlecode.com/svn/trunk/nltk/doc/book/errata.txt + +Data: +* added Europarl sample, with 10 docs for each of 11 langs (Nitin Madnani) +* added SMULTRON sample corpus (Torsten Marek, Martin Volk) + + +Version 2.0b7 2009-11-09 + +NLTK: +* minor bugfixes and enhancements: data loader, inference package, FreqDist, Punkt +* added Portuguese example module, similar to nltk.book for English (examples/pt.py) +* added all_lemma_names() method to WordNet corpus reader +* added update() and __add__() extensions to FreqDist (enhances alignment with Python 3.0 counters) +* reimplemented clean_html +* added test-suite runner for automatic/manual regression testing + +NLTK-Data: +* updated Punkt models for sentence segmentation +* added corpus of the works of Machado de Assis (Brazilian Portuguese) + +Book: +* Added translation of preface into Portuguese, contributed by Tiago Tresoldi. + +Version 2.0b6 2009-09-20 + +NLTK: +* minor fixes for Python 2.4 compatibility +* added words() method to XML corpus reader +* minor bugfixes and code clean-ups +* fixed downloader to put data in %APPDATA% on Windows + +Data: +* Updated Punkt models +* Fixed utf8 encoding issues with UDHR and Stopwords Corpora +* Renamed CoNLL "cat" files to "esp" (different language) +* Added Alvey NLT feature-based grammar +* Added Polish PL196x corpus + +Version 2.0b5 2009-07-19 + +NLTK: +* minor bugfixes (incl FreqDist, Python eggs) +* added reader for Europarl Corpora (contributed by Nitin Madnani) +* added reader for IPI PAN Polish Corpus (contributed by Konrad Goluchowski) +* fixed data.py so that it doesn't generate a warning for Windows Python 2.6 + +NLTK-Contrib: +* updated Praat reader (contributed by Margaret Mitchell) + +Version 2.0b4 2009-07-10 + +NLTK: +* switched to Apache License, Version 2.0 +* minor bugfixes in semantics and inference packages +* support for Python eggs +* fixed stale regression tests + +Data: +* added NomBank 1.0 +* uppercased feature names in some grammars + +Version 2.0b3 2009-06-25 + +NLTK: +* several bugfixes +* added nombank corpus reader (Paul Bedaride) + +Version 2.0b2 2009-06-15 + +NLTK: +* minor bugfixes and optimizations for parsers, updated some doctests +* added bottom-up filtered left corner parsers, + LeftCornerChartParser and IncrementalLeftCornerChartParser. +* fixed dispersion plot bug which prevented empty plots + +Version 2.0b1 2009-06-09 + +NLTK: +* major refactor of chart parser code and improved API (Peter Ljungl喃) +* added new bottom-up left-corner chart parser strategy +* misc bugfixes (ChunkScore, chart rules, chatbots, jcn-similarity) +* improved efficiency of "import nltk" using lazy module imports +* moved CCG package and ISRI Arabic stemmer from NLTK-Contrib into core NLTK +* misc code cleanups + +Contrib: +* moved out of the main NLTK distribution into a separate distribution + +Book: +* Ongoing polishing ahead of print publication + +Version 0.9.9 2009-05-06 + +NLTK: +* Finalized API for NLTK 2.0 and the book, incl dozens of small fixes +* Names of the form nltk.foo.Bar now available as nltk.Bar + for significant functionality; in some cases the name was modified + (using old names will produce a deprecation warning) +* Bugfixes in downloader, WordNet +* Expanded functionality in DecisionTree +* Bigram collocations extended for discontiguous bigrams +* Translation toy nltk.misc.babelfish +* New module nltk.help giving access to tagset documentation +* Fix imports so that NLTK builds without Tkinter (Bjorn Maeland) + +Data: +* new maxent NE chunker model +* updated grammar packages for the book +* data for new tagsets collection, documenting several tagsets +* added lolcat translation to the Genesis collection + +Contrib (work in progress): +* Updates to coreference package (Joseph Frazee) +* New ISRI Arabic stemmer (Hosam Algasaier) +* Updates to Toolbox package (Greg Aumann) + +Book: +* Substantial editorial corrections ahead of final submission + +Version 0.9.8 2009-02-18 + +NLTK: +* New off-the-shelf tokenizer, POS tagger, and named-entity tagger +* New metrics package with inter-annotator agreement scores, + distance metrics, rank correlation +* New collocations package (Joel Nothman) +* Many clean-ups to WordNet package (Steven Bethard, Jordan Boyd-Graber) +* Moved old pywordnet-based WordNet package to nltk_contrib +* WordNet browser (Paul Bone) +* New interface to dependency treebank corpora +* Moved MinimalSet class into nltk.misc package +* Put NLTK applications in new nltk.app package +* Many other improvements incl semantics package, toolbox, MaltParser +* Misc changes to many API names in preparation for 1.0, old names deprecated +* Most classes now available in the top-level namespace +* Work on Python egg distribution (Brandon Rhodes) +* Removed deprecated code remaining from 0.8.* versions +* Fixes for Python 2.4 compatibility + +Data: +* Corrected identifiers in Dependency Treebank corpus +* Basque and Catalan Dependency Treebanks (CoNLL 2007) +* PE08 Parser Evaluation data +* New models for POS tagger and named-entity tagger + +Book: +* Substantial editorial corrections + +Version 0.9.7 2008-12-19 + +NLTK: +* fixed problems with accessing zipped corpora +* improved design and efficiency of grammars and chart parsers + including new bottom-up combine strategy and a redesigned + Earley strategy (Peter Ljunglof) +* fixed bugs in smoothed probability distributions and added + regression tests (Peter Ljunglof) +* improvements to Punkt (Joel Nothman) +* improvements to text classifiers +* simple word-overlap RTE classifier + +Data: +* A new package of large grammars (Peter Ljunglof) +* A small gazetteer corpus and corpus reader +* Organized example grammars into separate packages +* Childrens' stories added to gutenberg package + +Contrib (work in progress): +* fixes and demonstration for named-entity feature extractors in nltk_contrib.coref + +Book: +* extensive changes throughout, including new chapter 5 on classification + and substantially revised chapter 11 on managing linguistic data + +Version 0.9.6 2008-12-07 + +NLTK: +* new WordNet corpus reader (contributed by Steven Bethard) +* incorporated dependency parsers into NLTK (was NLTK-Contrib) (contributed by Jason Narad) +* moved nltk/cfg.py to nltk/grammar.py and incorporated dependency grammars +* improved efficiency of unification algorithm +* various enhancements to the semantics package +* added plot() and tabulate() methods to FreqDist and ConditionalFreqDist +* FreqDist.keys() and list(FreqDist) provide keys reverse-sorted by value, + to avoid the confusion caused by FreqDist.sorted() +* new downloader module to support interactive data download: nltk.download() + run using "python -m nltk.downloader all" +* fixed WordNet bug that caused min_depth() to sometimes give incorrect result +* added nltk.util.Index as a wrapper around defaultdict(list) plus + a functional-style initializer +* fixed bug in Earley chart parser that caused it to break +* added basic TnT tagger nltk.tag.tnt +* new corpus reader for CoNLL dependency format (contributed by Kepa Sarasola and Iker Manterola) +* misc other bugfixes + +Contrib (work in progress): +* TIGERSearch implementation by Torsten Marek +* extensions to hole and glue semantics modules by Dan Garrette +* new coreference package by Joseph Frazee +* MapReduce interface by Xinfan Meng + +Data: +* Corpora are stored in compressed format if this will not compromise speed of access +* Swadesh Corpus of comparative wordlists in 23 languages +* Split grammar collection into separate packages +* New Basque and Spanish grammar samples (contributed by Kepa Sarasola and Iker Manterola) +* Brown Corpus sections now have meaningful names (e.g. 'a' is now 'news') +* Fixed bug that forced users to manually unzip the WordNet corpus +* New dependency-parsed version of Treebank corpus sample +* Added movie script "Monty Python and the Holy Grail" to webtext corpus +* Replaced words corpus data with a much larger list of English words +* New URL for list of available NLTK corpora + https://nltk.googlecode.com/svn/trunk/nltk_data/index.xml + +Book: +* complete rewrite of first three chapters to make the book accessible + to a wider audience +* new chapter on data-intensive language processing +* extensive reworking of most chapters +* Dropped subsection numbering; moved exercises to end of chapters + +Distributions: +* created Portfile to support Mac installation + + +Version 0.9.5 2008-08-27 + +NLTK: +* text module with support for concordancing, text generation, plotting +* book module +* Major reworking of the automated theorem proving modules (Dan Garrette) +* draw.dispersion now uses pylab +* draw.concordance GUI tool +* nltk.data supports for reading corpora and other data files from within zipfiles +* trees can be constructed from strings with Tree(s) (cf Tree.parse(s)) + +Contrib (work in progress): +* many updates to student projects + - nltk_contrib.agreement (Thomas Lippincott) + - nltk_contrib.coref (Joseph Frazee) + - nltk_contrib.depparser (Jason Narad) + - nltk_contrib.fuf (Petro Verkhogliad) + - nltk_contrib.hadoop (Xinfan Meng) +* clean-ups: deleted stale files; moved some packages to misc + +Data +* Cleaned up Gutenberg text corpora +* added Moby Dick; removed redundant copy of Blake songs. +* more tagger models +* renamed to nltk_data to facilitate installation +* stored each corpus as a zip file for quicker installation + and access, and to solve a problem with the Propbank + corpus including a file with an illegal name for MSWindows + (con.xml). + +Book: +* changed filenames to chNN format +* reworked opening chapters (work in progress) + +Distributions: +* fixed problem with mac installer that arose when Python binary + couldn't be found +* removed dependency of NLTK on nltk_data so that NLTK code can be + installed before the data + +Version 0.9.4 2008-08-01 + +NLTK: +- Expanded semantics package for first order logic, linear logic, + glue semantics, DRT, LFG (Dan Garrette) +- new WordSense class in wordnet.synset supporting access to synsets + from sense keys and accessing sense counts (Joel Nothman) +- interface to Mallet's linear chain CRF implementation (nltk.tag.crf) +- misc bugfixes incl Punkt, synsets, maxent +- improved support for chunkers incl flexible chunk corpus reader, + new rule type: ChunkRuleWithContext +- new GUI for pos-tagged concordancing nltk.draw.pos_concordance +- new GUI for developing regexp chunkers nltk.draw.rechunkparser +- added bio_sents() and bio_words() methods to ConllChunkCorpusReader in conll.py + to allow reading (word, tag, chunk_typ) tuples off of CoNLL-2000 corpus. Also + modified ConllChunkCorpusView to support these changes. +- feature structures support values with custom unification methods +- new flag on tagged corpus readers to use simplified tagsets +- new package for ngram language modeling with Katz backoff nltk.model +- added classes for single-parented and multi-parented trees that + automatically maintain parent pointers (nltk.tree.ParentedTree and + nltk.tree.MultiParentedTree) +- new WordNet browser GUI (Jussi Salmela, Paul Bone) +- improved support for lazy sequences +- added generate() method to probability distributions +- more flexible parser for converting bracketed strings to trees +- made fixes to docstrings to improve API documentation + +Contrib (work in progress) +- new NLG package, FUF/SURGE (Petro Verkhogliad) +- new dependency parser package (Jason Narad) +- new Coreference package, incl support for + ACE-2, MUC-6 and MUC-7 corpora (Joseph Frazee) +- CCG Parser (Graeme Gange) +- first order resolution theorem prover (Dan Garrette) + +Data: +- Nnw NPS Chat Corpus and corpus reader (nltk.corpus.nps_chat) +- ConllCorpusReader can now be used to read CoNLL 2004 and 2005 corpora. +- Implemented HMM-based Treebank POS tagger and phrase chunker for + nltk_contrib.coref in api.py. Pickled versions of these objects are checked + in in data/taggers and data/chunkers. + +Book: +- misc corrections in response to feedback from readers + +Version 0.9.3 2008-06-03 + +NLTK: +- modified WordNet similarity code to use pre-built information content files +- new classifier-based tagger, BNC corpus reader +- improved unicode support for corpus readers +- improved interfaces to Weka, Prover9/Mace4 +- new support for using MEGAM and SciPy to train maxent classifiers +- rewrite of Punkt sentence segmenter (Joel Nothman) +- bugfixes for WordNet information content module (Jordan Boyd-Graber) +- code clean-ups throughout + +Book: +- miscellaneous fixes in response to feedback from readers + +Contrib: +- implementation of incremental algorithm for generating + referring expressions (contributed by Margaret Mitchell) +- refactoring WordNet browser (Paul Bone) + +Corpora: +- included WordNet information content files + +Version 0.9.2 2008-03-04 + +NLTK: +- new theorem-prover and model-checker module nltk.inference, + including interface to Prover9/Mace4 (Dan Garrette, Ewan Klein) +- bugfix in Reuters corpus reader that causes Python + to complain about too many open files +- VerbNet and PropBank corpus readers + +Data: +- VerbNet Corpus version 2.1: hierarchical, verb lexicon linked to WordNet +- PropBank Corpus: predicate-argument structures, as stand-off annotation of Penn Treebank + +Contrib: +- New work on WordNet browser, incorporating a client-server model (Jussi Salmela) + +Distributions: +- Mac OS 10.5 distribution + +Version 0.9.1 2008-01-24 + +NLTK: +- new interface for text categorization corpora +- new corpus readers: RTE, Movie Reviews, Question Classification, Brown Corpus +- bugfix in ConcatenatedCorpusView that caused iteration to fail if it didn't start from the beginning of the corpus + +Data: +- Question classification data, included with permission of Li & Roth +- Reuters 21578 Corpus, ApteMod version, from CPAN +- Movie Reviews corpus (sentiment polarity), included with permission of Lillian Lee +- Corpus for Recognising Textual Entailment (RTE) Challenges 1, 2 and 3 +- Brown Corpus (reverted to original file structure: ca01-cr09) +- Penn Treebank corpus sample (simplified implementation, new readers treebank_raw and treebank_chunk) +- Minor redesign of corpus readers, to use filenames instead of "items" to identify parts of a corpus + +Contrib: +- theorem_prover: Prover9, tableau, MaltParser, Mace4, glue semantics, docs (Dan Garrette, Ewan Klein) +- drt: improved drawing, conversion to FOL (Dan Garrette) +- gluesemantics: GUI demonstration, abstracted LFG code, documentation (Dan Garrette) +- readability: various text readability scores (Thomas Jakobsen, Thomas Skardal) +- toolbox: code to normalize toolbox databases (Greg Aumann) + +Book: +- many improvements in early chapters in response to reader feedback +- updates for revised corpus readers +- moved unicode section to chapter 3 +- work on engineering.txt (not included in 0.9.1) + +Distributions: +- Fixed installation for Mac OS 10.5 (Joshua Ritterman) +- Generalize doctest_driver to work with doc_contrib + +Version 0.9 2007-10-12 + +NLTK: +- New naming of packages and modules, and more functions imported into + top-level nltk namespace, e.g. nltk.chunk.Regexp -> nltk.RegexpParser, + nltk.tokenize.Line -> nltk.LineTokenizer, nltk.stem.Porter -> nltk.PorterStemmer, + nltk.parse.ShiftReduce -> nltk.ShiftReduceParser +- processing class names changed from verbs to nouns, e.g. + StemI -> StemmerI, ParseI -> ParserI, ChunkParseI -> ChunkParserI, ClassifyI -> ClassifierI +- all tokenizers are now available as subclasses of TokenizeI, + selected tokenizers are also available as functions, e.g. wordpunct_tokenize() +- rewritten ngram tagger code, collapsed lookup tagger with unigram tagger +- improved tagger API, permitting training in the initializer +- new system for deprecating code so that users are notified of name changes. +- support for reading feature cfgs to parallel reading cfgs (parse_featcfg()) +- text classifier package, maxent (GIS, IIS), naive Bayes, decision trees, weka support +- more consistent tree printing +- wordnet's morphy stemmer now accessible via stemmer package +- RSLP Portuguese stemmer (originally developed by Viviane Moreira Orengo, reimplemented by Tiago Tresoldi) +- promoted ieer_rels.py to the sem package +- improvements to WordNet package (Jussi Salmela) +- more regression tests, and support for checking coverage of tests +- miscellaneous bugfixes +- remove numpy dependency + +Data: +- new corpus reader implementation, refactored syntax corpus readers +- new data package: corpora, grammars, tokenizers, stemmers, samples +- CESS-ESP Spanish Treebank and corpus reader +- CESS-CAT Catalan Treebank and corpus reader +- Alpino Dutch Treebank and corpus reader +- MacMorpho POS-tagged Brazilian Portuguese news text and corpus reader +- trained model for Portuguese sentence segmenter +- Floresta Portuguese Treebank version 7.4 and corpus reader +- TIMIT player audio support + +Contrib: +- BioReader (contributed by Carlos Rodriguez) +- TnT tagger (contributed by Sam Huston) +- wordnet browser (contributed by Jussi Salmela, requires wxpython) +- lpath interpreter (contributed by Haejoong Lee) +- timex -- regular expression-based temporal expression tagger + +Book: +- polishing of early chapters +- introductions to parts 1, 2, 3 +- improvements in book processing software (xrefs, avm & gloss formatting, javascript clipboard) +- updates to book organization, chapter contents +- corrections throughout suggested by readers (acknowledged in preface) +- more consistent use of US spelling throughout +- all examples redone to work with single import statement: "import nltk" +- reordered chapters: 5->7->8->9->11->12->5 + * language engineering in part 1 to broaden the appeal + of the earlier part of the book and to talk more about + evaluation and baselines at an earlier stage + * concentrate the partial and full parsing material in part 2, + and remove the specialized feature-grammar material into part 3 + +Distributions: +- streamlined mac installation (Joshua Ritterman) +- included mac distribution with ISO image + +Version 0.8 2007-07-01 + +Code: +- changed nltk.__init__ imports to explicitly import names from top-level modules +- changed corpus.util to use the 'rb' flag for opening files, to fix problems + reading corpora under MSWindows +- updated stale examples in engineering.txt +- extended feature structure interface to permit chained features, e.g. fs['F','G'] +- further misc improvements to test code plus some bugfixes +Tutorials: +- rewritten opening section of tagging chapter +- reorganized some exercises + +Version 0.8b2 2007-06-26 + +Code (major): +- new corpus package, obsoleting old corpora package + - supports caching, slicing, corpus search path + - more flexible API + - global updates so all NLTK modules use new corpus package +- moved nltk/contrib to separate top-level package nltk_contrib +- changed wordpunct tokenizer to use \w instead of a-zA-Z0-9 + as this will be more robust for languages other than English, + with implications for many corpus readers that use it +- known bug: certain re-entrant structures in featstruct +- known bug: when the LHS of an edge contains an ApplicationExpression, + variable values in the RHS bindings aren't copied over when the + fundamental rule applies +- known bug: HMM tagger is broken +Tutorials: +- global updates to NLTK and docs +- ongoing polishing +Corpora: +- treebank sample reverted to published multi-file structure +Contrib: +- DRT and Glue Semantics code (nltk_contrib.drt, nltk_contrib.gluesemantics, by Dan Garrette) + +Version 0.8b1 2007-06-18 + +Code (major): +- changed package name to nltk +- import all top-level modules into nltk, reducing need for import statements +- reorganization of sub-package structures to simplify imports +- new featstruct module, unifying old featurelite and featurestructure modules +- FreqDist now inherits from dict, fd.count(sample) becomes fd[sample] +- FreqDist initializer permits: fd = FreqDist(len(token) for token in text) +- made numpy optional +Code (minor): +- changed GrammarFile initializer to accept filename +- consistent tree display format +- fixed loading process for WordNet and TIMIT that prevented code installation if data not installed +- taken more care with unicode types +- incorporated pcfg code into cfg module +- moved cfg, tree, featstruct to top level +- new filebroker module to make handling of example grammar files more transparent +- more corpus readers (webtext, abc) +- added cfg.covers() to check that a grammar covers a sentence +- simple text-based wordnet browser +- known bug: parse/featurechart.py uses incorrect apply() function +Corpora: +- csv data file to document NLTK corpora +Contrib: +- added Glue semantics code (contrib.glue, by Dan Garrette) +- Punkt sentence segmenter port (contrib.punkt, by Willy) +- added LPath interpreter (contrib.lpath, by Haejoong Lee) +- extensive work on classifiers (contrib.classifier*, Sumukh Ghodke) +Tutorials: +- polishing on parts I, II +- more illustrations, data plots, summaries, exercises +- continuing to make prose more accessible to non-linguistic audience +- new default import that all chapters presume: from nltk.book import * +Distributions: +- updated to latest version of numpy +- removed WordNet installation instructions as WordNet is now included in corpus distribution +- added pylab (matplotlib) + +Version 0.7.5 2007-05-16 + +Code: +- improved WordNet and WordNet-Similarity interface +- the Lancaster Stemmer (contributed by Steven Tomcavage) +Corpora: +- Web text samples +- BioCreAtIvE-PPI - a corpus for protein-protein interactions +- Switchboard Telephone Speech Corpus Sample (via Talkbank) +- CMU Problem Reports Corpus sample +- CONLL2002 POS+NER data +- Patient Information Leaflet corpus +- WordNet 3.0 data files +- English wordlists: basic English, frequent words +Tutorials: +- more improvements to text and images + +Version 0.7.4 2007-05-01 + +Code: +- Indian POS tagged corpus reader: corpora.indian +- Sinica Treebank corpus reader: corpora.sinica_treebank +- new web corpus reader corpora.web +- tag package now supports pickling +- added function to utilities.py to guess character encoding +Corpora: +- Rotokas texts from Stuart Robinson +- POS-tagged corpora for several Indian languages (Bangla, Hindi, Marathi, Telugu) from A Kumaran +Tutorials: +- Substantial work on Part II of book on structured programming, parsing and grammar +- More bibliographic citations +- Improvements in typesetting, cross references +- Redimensioned images and tables for better use of page space +- Moved project list to wiki +Contrib: +- validation of toolbox entries using chunking +- improved classifiers +Distribution: +- updated for Python 2.5.1, Numpy 1.0.2 + +Version 0.7.3 2007-04-02 + +* Code: + - made chunk.Regexp.parse() more flexible about its input + - developed new syntax for PCFG grammars, e.g. A -> B C [0.3] | D [0.7] + - fixed CFG parser to support grammars with slash categories + - moved beta classify package from main NLTK to contrib + - Brill taggers loaded correctly + - misc bugfixes +* Corpora: + - Shakespeare XML corpus sample and corpus reader +* Tutorials: + - improvements to prose, exercises, plots, images + - expanded and reorganized tutorial on structured programming + - formatting improvements for Python listings + - improved plots (using pylab) + - categorization of problems by difficulty +Contrib: + - more work on kimmo lexicon and grammar + - more work on classifiers + +Version 0.7.2 2007-03-01 + +* Code: + - simple feature detectors (detect module) + - fixed problem when token generators are passed to a parser (parse package) + - fixed bug in Grammar.productions() (identified by Lucas Champollion and Mitch Marcus) + - fixed import bug in category.GrammarFile.earley_parser + - added utilities.OrderedDict + - initial port of old NLTK classifier package (by Sam Huston) + - UDHR corpus reader +* Corpora: + - added UDHR corpus (Universal Declaration of Human Rights) + with 10k text samples in 300+ languages +* Tutorials: + - improved images + - improved book formatting, including new support for: + - javascript to copy program examples to clipboard in HTML version, + - bibliography, chapter cross-references, colorization, index, table-of-contents + +* Contrib: + - new Kimmo system: contrib.mit.six863.kimmo (Rob Speer) + - fixes for: contrib.fsa (Rob Speer) + - demonstration of text classifiers trained on UDHR corpus for + language identification: contrib.langid (Sam Huston) + - new Lambek calculus system: contrib.lambek + - new tree implementation based on elementtree: contrib.tree + +Version 0.7.1 2007-01-14 + +* Code: + - bugfixes (HMM, WordNet) + +Version 0.7 2006-12-22 + +* Code: + - bugfixes, including fixed bug in Brown corpus reader + - cleaned up wordnet 2.1 interface code and similarity measures + - support for full Penn treebank format contributed by Yoav Goldberg +* Tutorials: + - expanded tutorials on advanced parsing and structured programming + - checked all doctest code + - improved images for chart parsing + +Version 0.7b1 2006-12-06 + +* Code: + - expanded semantic interpretation package + - new high-level chunking interface, with cascaded chunking + - split chunking code into new chunk package + - updated wordnet package to support version 2.1 of Wordnet. + - prototyped basic wordnet similarity measures + (path distance, Wu + Palmer and Leacock + Chodorow, Resnik similarity measures.) + - bugfixes (tag.Window, tag.ngram) + - more doctests +* Contrib: + - toolbox language settings module +* Tutorials: + - rewrite of chunking chapter, switched from Treebank to CoNLL format as main focus, + simplified evaluation framework, added ngram chunking section + - substantial updates throughout (esp programming and semantics chapters) +* Corpora: + - Chat-80 Prolog data files provided as corpora, plus corpus reader + +Version 0.7a2 2006-11-13 + +* Code: + - more doctest + - code to read Chat-80 data + - HMM bugfix +* Tutorials: + - continued updates and polishing +* Corpora: + - toolbox MDF sample data + +Version 0.7a1 2006-10-29 + +* Code: + - new toolbox module (Greg Aumann) + - new semantics package (Ewan Klein) + - bugfixes +* Tutorials + - substantial revision, especially in preface, introduction, words, + and semantics chapters. + +Version 0.6.6 2006-10-06 + +* Code: + - bugfixes (probability, shoebox, draw) +* Contrib: + - new work on shoebox package (Stuart Robinson) +* Tutorials: + - continual expansion and revision, especially on introduction to + programming, advanced programming and the feature-based grammar chapters. + +Version 0.6.5 2006-07-09 + +* Code: + - improvements to shoebox module (Stuart Robinson, Greg Aumann) + - incorporated feature-based parsing into core NLTK-Lite + - corpus reader for Sinica treebank sample + - new stemmer package +* Contrib: + - hole semantics implementation (Peter Wang) + - Incorporating yaml + - new work on feature structures, unification, lambda calculus + - new work on shoebox package (Stuart Robinson, Greg Aumann) +* Corpora: + - Sinica treebank sample +* Tutorials: + - expanded discussion throughout, incl: left-recursion, trees, grammars, + feature-based grammar, agreement, unification, PCFGs, + baseline performance, exercises, improved display of trees + +Version 0.6.4 2006-04-20 + +* Code: + - corpus readers for Senseval 2 and TIMIT + - clusterer (ported from old NLTK) + - support for cascaded chunkers + - bugfix suggested by Brent Payne + - new SortedDict class for regression testing +* Contrib: + - CombinedTagger tagger and marshalling taggers, contributed by Tiago Tresoldi +* Corpora: + - new: Senseval 2, TIMIT sample +* Tutorials: + - major revisions to programming, words, tagging, chunking, and parsing tutorials + - many new exercises + - formatting improvements, including colorized program examples + - fixed problem with testing on training data, reported by Jason Baldridge + +Version 0.6.3 2006-03-09 + +* switch to new style classes +* repair FSA model sufficiently for Kimmo module to work +* port of MIT Kimmo morphological analyzer; still needs lots of code clean-up and inline docs +* expanded support for shoebox format, developed with Stuart Robinson +* fixed bug in indexing CFG productions, for empty right-hand-sides +* efficiency improvements, suggested by Martin Ranang +* replaced classeq with isinstance, for efficiency improvement, as suggested by Martin Ranang +* bugfixes in chunk eval +* simplified call to draw_trees +* names, stopwords corpora + +Version 0.6.2 2006-01-29 + +* Peter Spiller's concordancer +* Will Hardy's implementation of Penton's paradigm visualization system +* corpus readers for presidential speeches +* removed NLTK dependency +* generalized CFG terminals to permit full range of characters +* used fully qualified names in demo code, for portability +* bugfixes from Yoav Goldberg, Eduardo Pereira Habkost +* fixed obscure quoting bug in tree displays and conversions +* simplified demo code, fixed import bug diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..8f4304b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,14 @@ +include *.md +include *.txt +include *.ini +include setup.* +include ChangeLog +include Makefile +include MANIFEST.in + + +graft nltk +graft tools +graft web + +global-exclude *~ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4a8a3b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# Natural Language Toolkit: source Makefile +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +PYTHON = python +VERSION = $(shell $(PYTHON) -c 'import nltk; print(nltk.__version__)' | sed '/^Warning: */d') +NLTK_URL = $(shell $(PYTHON) -c 'import nltk; print(nltk.__url__)' | sed '/^Warning: */d') + +.PHONY: all clean clean_code + +all: dist + +######################################################################## +# TESTING +######################################################################## +DOCTEST_FILES = nltk/test/*.doctest +DOCTEST_CODE_FILES = nltk/*.py nltk/*/*.py + +doctest: + pytest $(DOCTEST_FILES) + +doctest_code: + pytest $(DOCTEST_CODE_FILES) + +demotest: + find nltk -name "*.py"\ + -and -not -path *misc* \ + -and -not -name brown_ic.py \ + -exec echo ==== '{}' ==== \; -exec python '{}' \; + +######################################################################## +# DISTRIBUTIONS +######################################################################## + +dist: clean_code + $(PYTHON) -m build + +######################################################################## +# CLEAN +######################################################################## + +clean: clean_code + rm -rf build web/_build iso dist api MANIFEST nltk-$(VERSION) nltk.egg-info + +clean_code: + rm -f `find nltk -name '*.pyc'` + rm -f `find nltk -name '*.pyo'` + rm -f `find . -name '*~'` + rm -rf `find . -name '__pycache__'` + rm -f MANIFEST # regenerate manifest from MANIFEST.in diff --git a/README.md b/README.md new file mode 100644 index 0000000..adab69e --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Natural Language Toolkit (NLTK) +[![PyPI](https://img.shields.io/pypi/v/nltk.svg)](https://pypi.python.org/pypi/nltk) +![CI](https://github.com/nltk/nltk/actions/workflows/ci.yml/badge.svg?branch=develop) + +NLTK -- the Natural Language Toolkit -- is a suite of open source Python +modules, data sets, and tutorials supporting research and development in Natural +Language Processing. NLTK requires Python version from 3.10 up to the latest 3.14. + +For documentation, please visit [nltk.org](https://www.nltk.org/). + + +## Contributing + +Do you want to contribute to NLTK development? Great! +Please read [CONTRIBUTING.md](CONTRIBUTING.md) for more details. + +See also [how to contribute to NLTK](https://www.nltk.org/contribute.html). + + +## Donate + +Have you found the toolkit helpful? Please support NLTK development by donating +to the project via PayPal, using the link on the NLTK homepage. + + +## Citing + +If you publish work that uses NLTK, please cite the NLTK book, as follows: + + Bird, Steven, Edward Loper and Ewan Klein (2009). + Natural Language Processing with Python. O'Reilly Media Inc. + + +## Copyright + +Copyright (C) 2001-2026 NLTK Project + +For license information, see [LICENSE.txt](LICENSE.txt). + +[AUTHORS.md](AUTHORS.md) contains a list of everyone who has contributed to NLTK. + + +### Redistributing + +- NLTK source code is distributed under the Apache 2.0 License. +- NLTK documentation is distributed under the Creative Commons + Attribution-Noncommercial-No Derivative Works 3.0 United States license. +- NLTK corpora are provided under the terms given in the README file for each + corpus; all are redistributable and available for non-commercial use. +- NLTK may be freely redistributed, subject to the provisions of these licenses. diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..9d07db1 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`nltk/nltk` +- 原始仓库:https://github.com/nltk/nltk +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/RELEASE-Github.md b/RELEASE-Github.md new file mode 100644 index 0000000..2a59ce0 --- /dev/null +++ b/RELEASE-Github.md @@ -0,0 +1,46 @@ +# RELEASE-Github.md: Automated Release Workflow + +This document details the operation of the NLTK GitHub Actions release workflow, defined in `.github/workflows/release.yml`. This workflow automates the creation of GitHub releases and supports optional PyPI integration for official distribution. + +### Workflow Trigger + +The workflow is **event-driven**. It is automatically initiated upon pushing a git tag that follows the `v` prefix naming convention to the repository. + +* **Trigger Action:** `git push origin v3.10.x-rc` +* **Requirement:** The workflow file must be present on the target branch. **Crucially, the release tag must point to a commit located within `origin/develop` to ensure the release is built from the authorized development head.** + +### Prerequisites & Permissions + +Successful execution of this workflow requires specific repository configurations: + +* **Write Permissions:** Pushing a release tag requires **write** access to the repository, and the workflow needs `contents: write` (via `GITHUB_TOKEN`) to create the draft GitHub release. +* **Fork Execution:** If executing within a fork, ensure the repository settings under **Actions > General > Workflow permissions** are set to "Read and write permissions" to allow the workflow to create release objects. +* **Secrets:** No additional repository secrets are required; the workflow uses the automatically provisioned `GITHUB_TOKEN` to query CI and create the draft release. + +### Release Strategy: GitHub vs. PyPI + +The workflow maintains a clear separation between testing and production release artifacts: + +| Stage | Platform | Purpose | +| --- | --- | --- | +| **Release Candidate** | GitHub Releases | Early community feedback and environment validation. | +| **Official Release** | PyPI | Production distribution via `pip` and package repository indexing. | + +> **Integration:** While the workflow primarily stages artifacts on GitHub, it is capable of handling **PyPI integration**. If configured, the workflow will automatically publish the build artifacts to PyPI upon successful validation of the release tag. + +### Execution & Verification + +1. **Initiate Release:** Trigger the workflow by pushing the appropriate tag to the remote: + ```bash + git tag v3.10.0-rc1 + git push origin v3.10.0-rc1 + ``` + +2. **CI Validation:** As implemented in **[#3506](https://github.com/nltk/nltk/pull/3506)**, the workflow performs an automated check to verify that Continuous Integration (CI) has successfully passed on the specific commit to which the release tag points. +3. **Monitor Logs:** Follow the execution progress in the repository’s **Actions** tab. +4. **Audit:** If the workflow fails to push to PyPI or draft a release, inspect the logs for: + * **CI Failures:** Verify the commit status matches the requirements set forth in PR #3506. + * **Branch Validation:** Ensure the tagged commit is reachable via `origin/develop`. + * **Authentication errors:** Verify that the `PYPI_API_TOKEN` is active and has the correct scope. + * **Permission errors:** Confirm that the `GITHUB_TOKEN` has been granted the necessary write access. + * **Version conflicts:** Ensure the tag is not already associated with an existing release. diff --git a/RELEASE-HOWTO.txt b/RELEASE-HOWTO.txt new file mode 100644 index 0000000..1d0ece1 --- /dev/null +++ b/RELEASE-HOWTO.txt @@ -0,0 +1,85 @@ +Building an NLTK distribution +---------------------------------- + +0. Packages required to build, test, and distribute NLTK + - py312-pytest py312-requests py312-sphinx + - py312-numpy py312-matplotlib py312-tkinter py312-black + - sqlite3-tcl + - pip install sphinxcontrib-apidoc twython build twine pytest-mock pre-commit + +1. Testing + - Check no errors are reported in our continuous integration service: + https://github.com/nltk/nltk/actions + - Optionally test demonstration code locally + make demotest + - Optionally test individual modules: + tox -e py312 nltk.package.module + - Check the data index is up-to-date: + cd ../nltk_data; make; push + +2. Update Version Number and ChangeLog + - Update version number + edit nltk/VERSION and web/conf.py (version and release) + - Check web/install.rst mentions latest version of Python + - Check setup.py lists correct range of Python versions + - Add a new entry to the news page in nltk/web/news.rst + - Update the ChangeLog (for nltk, nltk_data) + git log "$(git describe --tags --abbrev=0)..HEAD" --oneline + edit ChangeLog + +3. Build Documentation + - Check the copyright year is correct and update if necessary + e.g. ./tools/global_replace.py 2001-2026 2001-2026 + check web/conf.py copyright line + - Check that installation instructions are up-to-date + (including the range of Python versions that are supported) + edit web/install.rst setup.py + - Ensure that nltk_theme is installed and updated + pip install -U nltk_theme + - Rebuild the API docs + sphinx-build -E ./web ./build + - Publish them + cd ../nltk.github.com; git pull (begin with current docs repo) + cp -r ../nltk/build/* . + git add . + git commit -m "updates for version 3.X.Y" + git push origin master + +4. Create a new version + - Tag this version: + cd ../nltk + git tag -a 3.X.Y -m "version 3.X.Y" + git push --tags + verify that it shows up here: https://github.com/nltk/nltk/tags + This is important for the website, as the footer will link to the + tag with the version from web/conf.py. + +5. Release + - Make the distributions + make clean; make dist; ls dist/ + - Upload the distributions + python -m twine upload dist/* + - Check upload + https://pypi.python.org/pypi/nltk + +6. Announce + - Post announcement to NLTK the mailing lists: + nltk-dev (for beta releases) + nltk-users (for final releases) + nltk twitter account + +7. Optionally update repo version + - we don't want builds from the repository to have the same release number + e.g. after release X.Y.4, update repository version to X.Y.5a (alpha) + + +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@@@ BOOK BUILD +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ + +The build requires docutils, pdflatex, python imaging library, epydoc, + cdrtools, ImageMagick + + 1. Check out a clean copy of the subversion repository (or make clean) + and install locally with pip install; make clean + 2. make doc (slow; see doc/ for the results) and commit diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6ade7cb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,123 @@ +# Security Policy + +## Reporting a Vulnerability + +Please report security issues to `nltk.team@gmail.com` + +## Security Hardening + +NLTK includes a centralized I/O security module (`nltk.pathsec`) that +validates file paths, network URLs, and zip archives. + +As of NLTK 3.10.0, strict enforcement is enabled by default +(`ENFORCE=True`). In normal operation, NLTK applies the stricter +`pathsec` policy unless a caller explicitly opts out. + +Under enforcement, unauthorized file access, SSRF attempts, and zip-slip +style path escapes raise exceptions (typically `PermissionError`) instead of emitting warnings. + +### Resource-loading security model + +NLTK's resource-loading protections are designed to reduce common risks +when NLTK is used with untrusted input or in shared environments such as +web applications, services, notebooks, CI/CD systems, and multi-tenant +pipelines. + +In particular, the current policy reduces the risk of: + +- **Arbitrary local file access through NLTK resource loading** by + requiring filesystem access to remain within allowed NLTK data + directories. +- **SSRF to non-public destinations** by resolving network targets and + blocking loopback, private, link-local, and multicast addresses. +- **Redirect-based bypasses** by re-validating redirects at each hop. +- **Zip-slip attacks** by validating extraction targets before writing + files. + +These protections apply to NLTK's own resource-loading paths and URL +handling. They are not a general operating-system sandbox, and they do +not prevent all unsafe behavior an application might perform outside +NLTK. + +### Local file access + +`file:` URLs are not a general-purpose mechanism for loading arbitrary +local files. + +With strict enforcement enabled (`ENFORCE=True`), file-backed resources +must resolve inside allowed NLTK data directories. By default these +directories are derived from: + +1. `nltk.data.path` (configurable at runtime) +2. `NLTK_DATA` environment variable +3. Standard locations (`~/nltk_data`, `/usr/share/nltk_data`, etc.) +4. The system temp directory + +If you use a custom resource directory, explicitly add it to +`nltk.data.path`: + +```python +import nltk +nltk.data.path.append('/my/custom/data') +``` + +Then load resources by NLTK resource path rather than relying on access +to arbitrary filesystem locations. + +### Current Working Directory (CWD) access + +Implicit access to the current working directory is not allowed under +strict enforcement (`ENFORCE=True`) unless that directory has been +explicitly added to `nltk.data.path`. + +If you intentionally want to trust the current directory, authorize it +explicitly: + +```python +import nltk +nltk.data.path.append('.') +``` + +This makes the trust decision explicit and avoids surprising behavior in +server-side or shared execution environments. + +### Network URL validation + +NLTK permits network resource loading only for `http:` and `https:` +URLs. + +Before a request is made, NLTK validates the resolved destination and +blocks requests to: + +- loopback addresses +- private RFC1918 ranges +- link-local addresses +- multicast addresses + +Redirects are re-validated at each hop, so a public URL cannot bypass +the policy by redirecting to a blocked destination. + +In practice, ordinary public URLs continue to work, while destinations +such as `127.0.0.1`, `10.0.0.0/8`, and `169.254.169.254` are rejected. + +### What is protected + +- **Path traversal**: file access is validated against allowed NLTK + data directories (`nltk.data.path`, `NLTK_DATA`, and standard system + locations). +- **SSRF prevention**: `urlopen` resolves hostnames via DNS and blocks + requests to loopback, private, link-local, and multicast IP ranges, + including obfuscated forms where applicable. +- **Zip-slip protection**: zip extraction validates that member paths + stay within the target directory. +- **Pickle safety**: `nltk.data.load()` uses `RestrictedUnpickler` + which blocks all class/function globals. Other pickle loading uses + `pickle_load()` which emits a security warning. + +### Note on symlinks + +NLTK's corpus readers perform lexical path containment checks when +joining file paths. These checks do not resolve symlinks. If your threat +model includes attackers who can place symlinks inside trusted NLTK data +directories, keep strict enforcement enabled so paths are fully resolved +and validated. diff --git a/nltk/VERSION b/nltk/VERSION new file mode 100644 index 0000000..30291cb --- /dev/null +++ b/nltk/VERSION @@ -0,0 +1 @@ +3.10.0 diff --git a/nltk/__init__.py b/nltk/__init__.py new file mode 100644 index 0000000..13b48ee --- /dev/null +++ b/nltk/__init__.py @@ -0,0 +1,207 @@ +# Natural Language Toolkit (NLTK) +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +The Natural Language Toolkit (NLTK) is an open source Python library +for Natural Language Processing. A free online book is available. +(If you use the library for academic research, please cite the book.) + +Steven Bird, Ewan Klein, and Edward Loper (2009). +Natural Language Processing with Python. O'Reilly Media Inc. +https://www.nltk.org/book/ + +isort:skip_file +""" + +import os +import importlib + +# ////////////////////////////////////////////////////// +# Metadata +# ////////////////////////////////////////////////////// + +# Version. For each new release, the version number should be updated +# in the file VERSION. +try: + # If a VERSION file exists, use it! + version_file = os.path.join(os.path.dirname(__file__), "VERSION") + with open(version_file) as infile: + __version__ = infile.read().strip() +except NameError: + __version__ = "unknown (running code interactively?)" +except OSError as ex: + __version__ = "unknown (%s)" % ex + +if __doc__ is not None: # fix for the ``python -OO`` + __doc__ += "\n@version: " + __version__ + + +# Copyright notice +__copyright__ = """\ +Copyright (C) 2001-2026 NLTK Project. + +Distributed and Licensed under the Apache License, Version 2.0, +which is included by reference. +""" + +__license__ = "Apache License, Version 2.0" +# Description of the toolkit, keywords, and the project's primary URL. +__longdescr__ = """\ +The Natural Language Toolkit (NLTK) is a Python package for +natural language processing. NLTK requires Python 3.10, 3.11, 3.12 or 3.13.""" +__keywords__ = [ + "NLP", + "CL", + "natural language processing", + "computational linguistics", + "parsing", + "tagging", + "tokenizing", + "syntax", + "linguistics", + "language", + "natural language", + "text analytics", +] +__url__ = "https://www.nltk.org/" + +# Maintainer, contributors, etc. +__maintainer__ = "NLTK Team" +__maintainer_email__ = "nltk.team@gmail.com" +__author__ = __maintainer__ +__author_email__ = __maintainer_email__ + +# "Trove" classifiers for Python Package Index. +__classifiers__ = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Scientific/Engineering :: Human Machine Interfaces", + "Topic :: Scientific/Engineering :: Information Analysis", + "Topic :: Text Processing", + "Topic :: Text Processing :: Filters", + "Topic :: Text Processing :: General", + "Topic :: Text Processing :: Indexing", + "Topic :: Text Processing :: Linguistic", +] + +from nltk.internals import config_java + +# support numpy from pypy +try: + import numpypy +except ImportError: + pass + +# Override missing methods on environments where it cannot be used like GAE. +import subprocess + +if not hasattr(subprocess, "PIPE"): + + def _fake_PIPE(*args, **kwargs): + raise NotImplementedError("subprocess.PIPE is not supported.") + + subprocess.PIPE = _fake_PIPE +if not hasattr(subprocess, "Popen"): + + def _fake_Popen(*args, **kwargs): + raise NotImplementedError("subprocess.Popen is not supported.") + + subprocess.Popen = _fake_Popen + +########################################################### +# TOP-LEVEL MODULES +########################################################### + +# Import top-level functionality into top-level namespace + +from nltk.collocations import * +from nltk.decorators import decorator, memoize +from nltk.featstruct import * +from nltk.grammar import * +from nltk.probability import * +from nltk.text import * +from nltk.util import * +from nltk.jsontags import * + +########################################################### +# PACKAGES +########################################################### + +from nltk.chunk import * +from nltk.classify import * +from nltk.inference import * +from nltk.metrics import * +from nltk.parse import * +from nltk.tag import * +from nltk.tokenize import * +from nltk.translate import * +from nltk.tree import * +from nltk.sem import * +from nltk.stem import * + +# Packages which can be lazily imported +# (a) we don't import * +# (b) they're slow to import or have run-time dependencies +# that can safely fail at run time + +from nltk import lazyimport + +app = lazyimport.LazyModule("app", locals(), globals()) +chat = lazyimport.LazyModule("chat", locals(), globals()) +corpus = lazyimport.LazyModule("corpus", locals(), globals()) +draw = lazyimport.LazyModule("draw", locals(), globals()) +toolbox = lazyimport.LazyModule("toolbox", locals(), globals()) + +# Optional loading +try: + import numpy +except ImportError: + pass +else: + from nltk import cluster + +from nltk.downloader import download, download_shell + +# Check if tkinter exists without importing it to avoid crashes after +# forks on macOS. Only nltk.app, nltk.draw, and demo modules should +# have top-level tkinter imports. See #2949 for more details. +if importlib.util.find_spec("tkinter"): + try: + from nltk.downloader import download_gui + except RuntimeError as e: + import warnings + + warnings.warn( + "Corpus downloader GUI not loaded " + "(RuntimeError during import: %s)" % str(e) + ) + +# explicitly import all top-level modules (ensuring +# they override the same names inadvertently imported +# from a subpackage) + +from nltk import ccg, chunk, classify, collocations +from nltk import data, featstruct, grammar, help, inference, metrics +from nltk import misc, parse, probability, sem, stem, wsd +from nltk import tag, tbl, text, tokenize, translate, tree, util + + +# FIXME: override any accidentally imported demo, see https://github.com/nltk/nltk/issues/2116 +def demo(): + print("To run the demo code for a module, type nltk.module.demo()") diff --git a/nltk/app/__init__.py b/nltk/app/__init__.py new file mode 100644 index 0000000..52bced9 --- /dev/null +++ b/nltk/app/__init__.py @@ -0,0 +1,47 @@ +# Natural Language Toolkit: Applications package +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Interactive NLTK Applications: + +chartparser: Chart Parser +chunkparser: Regular-Expression Chunk Parser +collocations: Find collocations in text +concordance: Part-of-speech concordancer +nemo: Finding (and Replacing) Nemo regular expression tool +rdparser: Recursive Descent Parser +srparser: Shift-Reduce Parser +wordnet: WordNet Browser +""" + + +# Import Tkinter-based modules if Tkinter is installed +try: + import tkinter +except ImportError: + import warnings + + warnings.warn("nltk.app package not loaded (please install Tkinter library).") +else: + from nltk.app.chartparser_app import app as chartparser + from nltk.app.chunkparser_app import app as chunkparser + from nltk.app.collocations_app import app as collocations + from nltk.app.concordance_app import app as concordance + from nltk.app.nemo_app import app as nemo + from nltk.app.rdparser_app import app as rdparser + from nltk.app.srparser_app import app as srparser + from nltk.app.wordnet_app import app as wordnet + + try: + from matplotlib import pylab + except ImportError: + import warnings + + warnings.warn("nltk.app.wordfreq not loaded (requires the matplotlib library).") + else: + from nltk.app.wordfreq_app import app as wordfreq diff --git a/nltk/app/chartparser_app.py b/nltk/app/chartparser_app.py new file mode 100644 index 0000000..39d12fc --- /dev/null +++ b/nltk/app/chartparser_app.py @@ -0,0 +1,2570 @@ +# Natural Language Toolkit: Chart Parser Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Jean Mark Gawron +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +A graphical tool for exploring chart parsing. + +Chart parsing is a flexible parsing algorithm that uses a data +structure called a "chart" to record hypotheses about syntactic +constituents. Each hypothesis is represented by a single "edge" on +the chart. A set of "chart rules" determine when new edges can be +added to the chart. This set of rules controls the overall behavior +of the parser (e.g. whether it parses top-down or bottom-up). + +The chart parsing tool demonstrates the process of parsing a single +sentence, with a given grammar and lexicon. Its display is divided +into three sections: the bottom section displays the chart; the middle +section displays the sentence; and the top section displays the +partial syntax tree corresponding to the selected edge. Buttons along +the bottom of the window are used to control the execution of the +algorithm. + +The chart parsing tool allows for flexible control of the parsing +algorithm. At each step of the algorithm, you can select which rule +or strategy you wish to apply. This allows you to experiment with +mixing different strategies (e.g. top-down and bottom-up). You can +exercise fine-grained control over the algorithm by selecting which +edge you wish to apply a rule to. +""" + +# At some point, we should rewrite this tool to use the new canvas +# widget system. + + +import os.path +import pickle +from tkinter import ( + Button, + Canvas, + Checkbutton, + Frame, + IntVar, + Label, + Menu, + Scrollbar, + Tk, + Toplevel, +) +from tkinter.filedialog import askopenfilename, asksaveasfilename +from tkinter.font import Font +from tkinter.messagebox import showerror, showinfo + +from nltk.draw import CFGEditor, TreeSegmentWidget, tree_to_treesegment +from nltk.draw.util import ( + CanvasFrame, + ColorizedList, + EntryDialog, + MutableOptionMenu, + ShowText, + SymbolWidget, +) +from nltk.grammar import CFG, Nonterminal +from nltk.parse.chart import ( + BottomUpPredictCombineRule, + BottomUpPredictRule, + Chart, + LeafEdge, + LeafInitRule, + SingleEdgeFundamentalRule, + SteppingChartParser, + TopDownInitRule, + TopDownPredictRule, + TreeEdge, +) +from nltk.picklesec import pickle_load +from nltk.tree import Tree +from nltk.util import in_idle + +# Known bug: ChartView doesn't handle edges generated by epsilon +# productions (e.g., [Production: PP -> ]) very well. + +####################################################################### +# Edge List +####################################################################### + + +class EdgeList(ColorizedList): + ARROW = SymbolWidget.SYMBOLS["rightarrow"] + + def _init_colortags(self, textwidget, options): + textwidget.tag_config("terminal", foreground="#006000") + textwidget.tag_config("arrow", font="symbol", underline="0") + textwidget.tag_config("dot", foreground="#000000") + textwidget.tag_config( + "nonterminal", foreground="blue", font=("helvetica", -12, "bold") + ) + + def _item_repr(self, item): + contents = [] + contents.append(("%s\t" % item.lhs(), "nonterminal")) + contents.append((self.ARROW, "arrow")) + for i, elt in enumerate(item.rhs()): + if i == item.dot(): + contents.append((" *", "dot")) + if isinstance(elt, Nonterminal): + contents.append((" %s" % elt.symbol(), "nonterminal")) + else: + contents.append((" %r" % elt, "terminal")) + if item.is_complete(): + contents.append((" *", "dot")) + return contents + + +####################################################################### +# Chart Matrix View +####################################################################### + + +class ChartMatrixView: + """ + A view of a chart that displays the contents of the corresponding matrix. + """ + + def __init__( + self, parent, chart, toplevel=True, title="Chart Matrix", show_numedges=False + ): + self._chart = chart + self._cells = [] + self._marks = [] + + self._selected_cell = None + + if toplevel: + self._root = Toplevel(parent) + self._root.title(title) + self._root.bind("", self.destroy) + self._init_quit(self._root) + else: + self._root = Frame(parent) + + self._init_matrix(self._root) + self._init_list(self._root) + if show_numedges: + self._init_numedges(self._root) + else: + self._numedges_label = None + + self._callbacks = {} + + self._num_edges = 0 + + self.draw() + + def _init_quit(self, root): + quit = Button(root, text="Quit", command=self.destroy) + quit.pack(side="bottom", expand=0, fill="none") + + def _init_matrix(self, root): + cframe = Frame(root, border=2, relief="sunken") + cframe.pack(expand=0, fill="none", padx=1, pady=3, side="top") + self._canvas = Canvas(cframe, width=200, height=200, background="white") + self._canvas.pack(expand=0, fill="none") + + def _init_numedges(self, root): + self._numedges_label = Label(root, text="0 edges") + self._numedges_label.pack(expand=0, fill="none", side="top") + + def _init_list(self, root): + self._list = EdgeList(root, [], width=20, height=5) + self._list.pack(side="top", expand=1, fill="both", pady=3) + + def cb(edge, self=self): + self._fire_callbacks("select", edge) + + self._list.add_callback("select", cb) + self._list.focus() + + def destroy(self, *e): + if self._root is None: + return + try: + self._root.destroy() + except Exception: + pass + self._root = None + + def set_chart(self, chart): + if chart is not self._chart: + self._chart = chart + self._num_edges = 0 + self.draw() + + def update(self): + if self._root is None: + return + + # Count the edges in each cell + N = len(self._cells) + cell_edges = [[0 for i in range(N)] for j in range(N)] + for edge in self._chart: + cell_edges[edge.start()][edge.end()] += 1 + + # Color the cells correspondingly. + for i in range(N): + for j in range(i, N): + if cell_edges[i][j] == 0: + color = "gray20" + else: + color = "#00{:02x}{:02x}".format( + min(255, 50 + 128 * cell_edges[i][j] / 10), + max(0, 128 - 128 * cell_edges[i][j] / 10), + ) + cell_tag = self._cells[i][j] + self._canvas.itemconfig(cell_tag, fill=color) + if (i, j) == self._selected_cell: + self._canvas.itemconfig(cell_tag, outline="#00ffff", width=3) + self._canvas.tag_raise(cell_tag) + else: + self._canvas.itemconfig(cell_tag, outline="black", width=1) + + # Update the edge list. + edges = list(self._chart.select(span=self._selected_cell)) + self._list.set(edges) + + # Update our edge count. + self._num_edges = self._chart.num_edges() + if self._numedges_label is not None: + self._numedges_label["text"] = "%d edges" % self._num_edges + + def activate(self): + self._canvas.itemconfig("inactivebox", state="hidden") + self.update() + + def inactivate(self): + self._canvas.itemconfig("inactivebox", state="normal") + self.update() + + def add_callback(self, event, func): + self._callbacks.setdefault(event, {})[func] = 1 + + def remove_callback(self, event, func=None): + if func is None: + del self._callbacks[event] + else: + try: + del self._callbacks[event][func] + except KeyError: + pass + + def _fire_callbacks(self, event, *args): + if event not in self._callbacks: + return + for cb_func in list(self._callbacks[event].keys()): + cb_func(*args) + + def select_cell(self, i, j): + if self._root is None: + return + + # If the cell is already selected (and the chart contents + # haven't changed), then do nothing. + if (i, j) == self._selected_cell and self._chart.num_edges() == self._num_edges: + return + + self._selected_cell = (i, j) + self.update() + + # Fire the callback. + self._fire_callbacks("select_cell", i, j) + + def deselect_cell(self): + if self._root is None: + return + self._selected_cell = None + self._list.set([]) + self.update() + + def _click_cell(self, i, j): + if self._selected_cell == (i, j): + self.deselect_cell() + else: + self.select_cell(i, j) + + def view_edge(self, edge): + self.select_cell(*edge.span()) + self._list.view(edge) + + def mark_edge(self, edge): + if self._root is None: + return + self.select_cell(*edge.span()) + self._list.mark(edge) + + def unmark_edge(self, edge=None): + if self._root is None: + return + self._list.unmark(edge) + + def markonly_edge(self, edge): + if self._root is None: + return + self.select_cell(*edge.span()) + self._list.markonly(edge) + + def draw(self): + if self._root is None: + return + LEFT_MARGIN = BOT_MARGIN = 15 + TOP_MARGIN = 5 + c = self._canvas + c.delete("all") + N = self._chart.num_leaves() + 1 + dx = (int(c["width"]) - LEFT_MARGIN) / N + dy = (int(c["height"]) - TOP_MARGIN - BOT_MARGIN) / N + + c.delete("all") + + # Labels and dotted lines + for i in range(N): + c.create_text( + LEFT_MARGIN - 2, i * dy + dy / 2 + TOP_MARGIN, text=repr(i), anchor="e" + ) + c.create_text( + i * dx + dx / 2 + LEFT_MARGIN, + N * dy + TOP_MARGIN + 1, + text=repr(i), + anchor="n", + ) + c.create_line( + LEFT_MARGIN, + dy * (i + 1) + TOP_MARGIN, + dx * N + LEFT_MARGIN, + dy * (i + 1) + TOP_MARGIN, + dash=".", + ) + c.create_line( + dx * i + LEFT_MARGIN, + TOP_MARGIN, + dx * i + LEFT_MARGIN, + dy * N + TOP_MARGIN, + dash=".", + ) + + # A box around the whole thing + c.create_rectangle( + LEFT_MARGIN, TOP_MARGIN, LEFT_MARGIN + dx * N, dy * N + TOP_MARGIN, width=2 + ) + + # Cells + self._cells = [[None for i in range(N)] for j in range(N)] + for i in range(N): + for j in range(i, N): + t = c.create_rectangle( + j * dx + LEFT_MARGIN, + i * dy + TOP_MARGIN, + (j + 1) * dx + LEFT_MARGIN, + (i + 1) * dy + TOP_MARGIN, + fill="gray20", + ) + self._cells[i][j] = t + + def cb(event, self=self, i=i, j=j): + self._click_cell(i, j) + + c.tag_bind(t, "", cb) + + # Inactive box + xmax, ymax = int(c["width"]), int(c["height"]) + t = c.create_rectangle( + -100, + -100, + xmax + 100, + ymax + 100, + fill="gray50", + state="hidden", + tag="inactivebox", + ) + c.tag_lower(t) + + # Update the cells. + self.update() + + def pack(self, *args, **kwargs): + self._root.pack(*args, **kwargs) + + +####################################################################### +# Chart Results View +####################################################################### + + +class ChartResultsView: + def __init__(self, parent, chart, grammar, toplevel=True): + self._chart = chart + self._grammar = grammar + self._trees = [] + self._y = 10 + self._treewidgets = [] + self._selection = None + self._selectbox = None + + if toplevel: + self._root = Toplevel(parent) + self._root.title("Chart Parser Application: Results") + self._root.bind("", self.destroy) + else: + self._root = Frame(parent) + + # Buttons + if toplevel: + buttons = Frame(self._root) + buttons.pack(side="bottom", expand=0, fill="x") + Button(buttons, text="Quit", command=self.destroy).pack(side="right") + Button(buttons, text="Print All", command=self.print_all).pack(side="left") + Button(buttons, text="Print Selection", command=self.print_selection).pack( + side="left" + ) + + # Canvas frame. + self._cframe = CanvasFrame(self._root, closeenough=20) + self._cframe.pack(side="top", expand=1, fill="both") + + # Initial update + self.update() + + def update(self, edge=None): + if self._root is None: + return + # If the edge isn't a parse edge, do nothing. + if edge is not None: + if edge.lhs() != self._grammar.start(): + return + if edge.span() != (0, self._chart.num_leaves()): + return + + for parse in self._chart.parses(self._grammar.start()): + if parse not in self._trees: + self._add(parse) + + def _add(self, parse): + # Add it to self._trees. + self._trees.append(parse) + + # Create a widget for it. + c = self._cframe.canvas() + treewidget = tree_to_treesegment(c, parse) + + # Add it to the canvas frame. + self._treewidgets.append(treewidget) + self._cframe.add_widget(treewidget, 10, self._y) + + # Register callbacks. + treewidget.bind_click(self._click) + + # Update y. + self._y = treewidget.bbox()[3] + 10 + + def _click(self, widget): + c = self._cframe.canvas() + if self._selection is not None: + c.delete(self._selectbox) + self._selection = widget + (x1, y1, x2, y2) = widget.bbox() + self._selectbox = c.create_rectangle(x1, y1, x2, y2, width=2, outline="#088") + + def _color(self, treewidget, color): + treewidget.label()["color"] = color + for child in treewidget.subtrees(): + if isinstance(child, TreeSegmentWidget): + self._color(child, color) + else: + child["color"] = color + + def print_all(self, *e): + if self._root is None: + return + self._cframe.print_to_file() + + def print_selection(self, *e): + if self._root is None: + return + if self._selection is None: + showerror("Print Error", "No tree selected") + else: + c = self._cframe.canvas() + for widget in self._treewidgets: + if widget is not self._selection: + self._cframe.destroy_widget(widget) + c.delete(self._selectbox) + (x1, y1, x2, y2) = self._selection.bbox() + self._selection.move(10 - x1, 10 - y1) + c["scrollregion"] = f"0 0 {x2 - x1 + 20} {y2 - y1 + 20}" + self._cframe.print_to_file() + + # Restore our state. + self._treewidgets = [self._selection] + self.clear() + self.update() + + def clear(self): + if self._root is None: + return + for treewidget in self._treewidgets: + self._cframe.destroy_widget(treewidget) + self._trees = [] + self._treewidgets = [] + if self._selection is not None: + self._cframe.canvas().delete(self._selectbox) + self._selection = None + self._y = 10 + + def set_chart(self, chart): + self.clear() + self._chart = chart + self.update() + + def set_grammar(self, grammar): + self.clear() + self._grammar = grammar + self.update() + + def destroy(self, *e): + if self._root is None: + return + try: + self._root.destroy() + except Exception: + pass + self._root = None + + def pack(self, *args, **kwargs): + self._root.pack(*args, **kwargs) + + +####################################################################### +# Chart Comparer +####################################################################### + + +class ChartComparer: + """ + + :ivar _root: The root window + + :ivar _charts: A dictionary mapping names to charts. When + charts are loaded, they are added to this dictionary. + + :ivar _left_chart: The left ``Chart``. + :ivar _left_name: The name ``_left_chart`` (derived from filename) + :ivar _left_matrix: The ``ChartMatrixView`` for ``_left_chart`` + :ivar _left_selector: The drop-down ``MutableOptionsMenu`` used + to select ``_left_chart``. + + :ivar _right_chart: The right ``Chart``. + :ivar _right_name: The name ``_right_chart`` (derived from filename) + :ivar _right_matrix: The ``ChartMatrixView`` for ``_right_chart`` + :ivar _right_selector: The drop-down ``MutableOptionsMenu`` used + to select ``_right_chart``. + + :ivar _out_chart: The out ``Chart``. + :ivar _out_name: The name ``_out_chart`` (derived from filename) + :ivar _out_matrix: The ``ChartMatrixView`` for ``_out_chart`` + :ivar _out_label: The label for ``_out_chart``. + + :ivar _op_label: A Label containing the most recent operation. + """ + + _OPSYMBOL = { + "-": "-", + "and": SymbolWidget.SYMBOLS["intersection"], + "or": SymbolWidget.SYMBOLS["union"], + } + + def __init__(self, *chart_filenames): + # This chart is displayed when we don't have a value (eg + # before any chart is loaded). + faketok = [""] * 8 + self._emptychart = Chart(faketok) + + # The left & right charts start out empty. + self._left_name = "None" + self._right_name = "None" + self._left_chart = self._emptychart + self._right_chart = self._emptychart + + # The charts that have been loaded. + self._charts = {"None": self._emptychart} + + # The output chart. + self._out_chart = self._emptychart + + # The most recent operation + self._operator = None + + # Set up the root window. + self._root = Tk() + self._root.title("Chart Comparison") + self._root.bind("", self.destroy) + self._root.bind("", self.destroy) + + # Initialize all widgets, etc. + self._init_menubar(self._root) + self._init_chartviews(self._root) + self._init_divider(self._root) + self._init_buttons(self._root) + self._init_bindings(self._root) + + # Load any specified charts. + for filename in chart_filenames: + self.load_chart(filename) + + def destroy(self, *e): + if self._root is None: + return + try: + self._root.destroy() + except Exception: + pass + self._root = None + + def mainloop(self, *args, **kwargs): + return + self._root.mainloop(*args, **kwargs) + + # //////////////////////////////////////////////////////////// + # Initialization + # //////////////////////////////////////////////////////////// + + def _init_menubar(self, root): + menubar = Menu(root) + + # File menu + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Load Chart", + accelerator="Ctrl-o", + underline=0, + command=self.load_chart_dialog, + ) + filemenu.add_command( + label="Save Output", + accelerator="Ctrl-s", + underline=0, + command=self.save_chart_dialog, + ) + filemenu.add_separator() + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + # Compare menu + opmenu = Menu(menubar, tearoff=0) + opmenu.add_command( + label="Intersection", command=self._intersection, accelerator="+" + ) + opmenu.add_command(label="Union", command=self._union, accelerator="*") + opmenu.add_command( + label="Difference", command=self._difference, accelerator="-" + ) + opmenu.add_separator() + opmenu.add_command(label="Swap Charts", command=self._swapcharts) + menubar.add_cascade(label="Compare", underline=0, menu=opmenu) + + # Add the menu + self._root.config(menu=menubar) + + def _init_divider(self, root): + divider = Frame(root, border=2, relief="sunken") + divider.pack(side="top", fill="x", ipady=2) + + def _init_chartviews(self, root): + opfont = ("symbol", -36) # Font for operator. + eqfont = ("helvetica", -36) # Font for equals sign. + + frame = Frame(root, background="#c0c0c0") + frame.pack(side="top", expand=1, fill="both") + + # The left matrix. + cv1_frame = Frame(frame, border=3, relief="groove") + cv1_frame.pack(side="left", padx=8, pady=7, expand=1, fill="both") + self._left_selector = MutableOptionMenu( + cv1_frame, list(self._charts.keys()), command=self._select_left + ) + self._left_selector.pack(side="top", pady=5, fill="x") + self._left_matrix = ChartMatrixView( + cv1_frame, self._emptychart, toplevel=False, show_numedges=True + ) + self._left_matrix.pack(side="bottom", padx=5, pady=5, expand=1, fill="both") + self._left_matrix.add_callback("select", self.select_edge) + self._left_matrix.add_callback("select_cell", self.select_cell) + self._left_matrix.inactivate() + + # The operator. + self._op_label = Label( + frame, text=" ", width=3, background="#c0c0c0", font=opfont + ) + self._op_label.pack(side="left", padx=5, pady=5) + + # The right matrix. + cv2_frame = Frame(frame, border=3, relief="groove") + cv2_frame.pack(side="left", padx=8, pady=7, expand=1, fill="both") + self._right_selector = MutableOptionMenu( + cv2_frame, list(self._charts.keys()), command=self._select_right + ) + self._right_selector.pack(side="top", pady=5, fill="x") + self._right_matrix = ChartMatrixView( + cv2_frame, self._emptychart, toplevel=False, show_numedges=True + ) + self._right_matrix.pack(side="bottom", padx=5, pady=5, expand=1, fill="both") + self._right_matrix.add_callback("select", self.select_edge) + self._right_matrix.add_callback("select_cell", self.select_cell) + self._right_matrix.inactivate() + + # The equals sign + Label(frame, text="=", width=3, background="#c0c0c0", font=eqfont).pack( + side="left", padx=5, pady=5 + ) + + # The output matrix. + out_frame = Frame(frame, border=3, relief="groove") + out_frame.pack(side="left", padx=8, pady=7, expand=1, fill="both") + self._out_label = Label(out_frame, text="Output") + self._out_label.pack(side="top", pady=9) + self._out_matrix = ChartMatrixView( + out_frame, self._emptychart, toplevel=False, show_numedges=True + ) + self._out_matrix.pack(side="bottom", padx=5, pady=5, expand=1, fill="both") + self._out_matrix.add_callback("select", self.select_edge) + self._out_matrix.add_callback("select_cell", self.select_cell) + self._out_matrix.inactivate() + + def _init_buttons(self, root): + buttons = Frame(root) + buttons.pack(side="bottom", pady=5, fill="x", expand=0) + Button(buttons, text="Intersection", command=self._intersection).pack( + side="left" + ) + Button(buttons, text="Union", command=self._union).pack(side="left") + Button(buttons, text="Difference", command=self._difference).pack(side="left") + Frame(buttons, width=20).pack(side="left") + Button(buttons, text="Swap Charts", command=self._swapcharts).pack(side="left") + + Button(buttons, text="Detach Output", command=self._detach_out).pack( + side="right" + ) + + def _init_bindings(self, root): + # root.bind('', self.save_chart) + root.bind("", self.load_chart_dialog) + # root.bind('', self.reset) + + # //////////////////////////////////////////////////////////// + # Input Handling + # //////////////////////////////////////////////////////////// + + def _select_left(self, name): + self._left_name = name + self._left_chart = self._charts[name] + self._left_matrix.set_chart(self._left_chart) + if name == "None": + self._left_matrix.inactivate() + self._apply_op() + + def _select_right(self, name): + self._right_name = name + self._right_chart = self._charts[name] + self._right_matrix.set_chart(self._right_chart) + if name == "None": + self._right_matrix.inactivate() + self._apply_op() + + def _apply_op(self): + if self._operator == "-": + self._difference() + elif self._operator == "or": + self._union() + elif self._operator == "and": + self._intersection() + + # //////////////////////////////////////////////////////////// + # File + # //////////////////////////////////////////////////////////// + CHART_FILE_TYPES = [("Pickle file", ".pickle"), ("All files", "*")] + + def save_chart_dialog(self, *args): + filename = asksaveasfilename( + filetypes=self.CHART_FILE_TYPES, defaultextension=".pickle" + ) + if not filename: + return + try: + with open(filename, "wb") as outfile: + pickle.dump(self._out_chart, outfile) + except Exception as e: + showerror("Error Saving Chart", f"Unable to open file: {filename!r}\n{e}") + + def load_chart_dialog(self, *args): + filename = askopenfilename( + filetypes=self.CHART_FILE_TYPES, defaultextension=".pickle" + ) + if not filename: + return + try: + self.load_chart(filename) + except Exception as e: + showerror("Error Loading Chart", f"Unable to open file: {filename!r}\n{e}") + + def load_chart(self, filename): + with open(filename, "rb") as infile: + chart = pickle_load(infile) + name = os.path.basename(filename) + if name.endswith(".pickle"): + name = name[:-7] + if name.endswith(".chart"): + name = name[:-6] + self._charts[name] = chart + self._left_selector.add(name) + self._right_selector.add(name) + + # If either left_matrix or right_matrix is empty, then + # display the new chart. + if self._left_chart is self._emptychart: + self._left_selector.set(name) + elif self._right_chart is self._emptychart: + self._right_selector.set(name) + + def _update_chartviews(self): + self._left_matrix.update() + self._right_matrix.update() + self._out_matrix.update() + + # //////////////////////////////////////////////////////////// + # Selection + # //////////////////////////////////////////////////////////// + + def select_edge(self, edge): + if edge in self._left_chart: + self._left_matrix.markonly_edge(edge) + else: + self._left_matrix.unmark_edge() + if edge in self._right_chart: + self._right_matrix.markonly_edge(edge) + else: + self._right_matrix.unmark_edge() + if edge in self._out_chart: + self._out_matrix.markonly_edge(edge) + else: + self._out_matrix.unmark_edge() + + def select_cell(self, i, j): + self._left_matrix.select_cell(i, j) + self._right_matrix.select_cell(i, j) + self._out_matrix.select_cell(i, j) + + # //////////////////////////////////////////////////////////// + # Operations + # //////////////////////////////////////////////////////////// + + def _difference(self): + if not self._checkcompat(): + return + + out_chart = Chart(self._left_chart.tokens()) + for edge in self._left_chart: + if edge not in self._right_chart: + out_chart.insert(edge, []) + + self._update("-", out_chart) + + def _intersection(self): + if not self._checkcompat(): + return + + out_chart = Chart(self._left_chart.tokens()) + for edge in self._left_chart: + if edge in self._right_chart: + out_chart.insert(edge, []) + + self._update("and", out_chart) + + def _union(self): + if not self._checkcompat(): + return + + out_chart = Chart(self._left_chart.tokens()) + for edge in self._left_chart: + out_chart.insert(edge, []) + for edge in self._right_chart: + out_chart.insert(edge, []) + + self._update("or", out_chart) + + def _swapcharts(self): + left, right = self._left_name, self._right_name + self._left_selector.set(right) + self._right_selector.set(left) + + def _checkcompat(self): + if ( + self._left_chart.tokens() != self._right_chart.tokens() + or self._left_chart.property_names() != self._right_chart.property_names() + or self._left_chart == self._emptychart + or self._right_chart == self._emptychart + ): + # Clear & inactivate the output chart. + self._out_chart = self._emptychart + self._out_matrix.set_chart(self._out_chart) + self._out_matrix.inactivate() + self._out_label["text"] = "Output" + # Issue some other warning? + return False + else: + return True + + def _update(self, operator, out_chart): + self._operator = operator + self._op_label["text"] = self._OPSYMBOL[operator] + self._out_chart = out_chart + self._out_matrix.set_chart(out_chart) + self._out_label["text"] = "{} {} {}".format( + self._left_name, + self._operator, + self._right_name, + ) + + def _clear_out_chart(self): + self._out_chart = self._emptychart + self._out_matrix.set_chart(self._out_chart) + self._op_label["text"] = " " + self._out_matrix.inactivate() + + def _detach_out(self): + ChartMatrixView(self._root, self._out_chart, title=self._out_label["text"]) + + +####################################################################### +# Chart View +####################################################################### + + +class ChartView: + """ + A component for viewing charts. This is used by ``ChartParserApp`` to + allow students to interactively experiment with various chart + parsing techniques. It is also used by ``Chart.draw()``. + + :ivar _chart: The chart that we are giving a view of. This chart + may be modified; after it is modified, you should call + ``update``. + :ivar _sentence: The list of tokens that the chart spans. + + :ivar _root: The root window. + :ivar _chart_canvas: The canvas we're using to display the chart + itself. + :ivar _tree_canvas: The canvas we're using to display the tree + that each edge spans. May be None, if we're not displaying + trees. + :ivar _sentence_canvas: The canvas we're using to display the sentence + text. May be None, if we're not displaying the sentence text. + :ivar _edgetags: A dictionary mapping from edges to the tags of + the canvas elements (lines, etc) used to display that edge. + The values of this dictionary have the form + ``(linetag, rhstag1, dottag, rhstag2, lhstag)``. + :ivar _treetags: A list of all the tags that make up the tree; + used to erase the tree (without erasing the loclines). + :ivar _chart_height: The height of the chart canvas. + :ivar _sentence_height: The height of the sentence canvas. + :ivar _tree_height: The height of the tree + + :ivar _text_height: The height of a text string (in the normal + font). + + :ivar _edgelevels: A list of edges at each level of the chart (the + top level is the 0th element). This list is used to remember + where edges should be drawn; and to make sure that no edges + are overlapping on the chart view. + + :ivar _unitsize: Pixel size of one unit (from the location). This + is determined by the span of the chart's location, and the + width of the chart display canvas. + + :ivar _fontsize: The current font size + + :ivar _marks: A dictionary from edges to marks. Marks are + strings, specifying colors (e.g. 'green'). + """ + + _LEAF_SPACING = 10 + _MARGIN = 10 + _TREE_LEVEL_SIZE = 12 + _CHART_LEVEL_SIZE = 40 + + def __init__(self, chart, root=None, **kw): + """ + Construct a new ``Chart`` display. + """ + # Process keyword args. + draw_tree = kw.get("draw_tree", 0) + draw_sentence = kw.get("draw_sentence", 1) + self._fontsize = kw.get("fontsize", -12) + + # The chart! + self._chart = chart + + # Callback functions + self._callbacks = {} + + # Keep track of drawn edges + self._edgelevels = [] + self._edgetags = {} + + # Keep track of which edges are marked. + self._marks = {} + + # These are used to keep track of the set of tree tokens + # currently displayed in the tree canvas. + self._treetoks = [] + self._treetoks_edge = None + self._treetoks_index = 0 + + # Keep track of the tags used to draw the tree + self._tree_tags = [] + + # Put multiple edges on each level? + self._compact = 0 + + # If they didn't provide a main window, then set one up. + if root is None: + top = Tk() + top.title("Chart View") + + def destroy1(e, top=top): + top.destroy() + + def destroy2(top=top): + top.destroy() + + top.bind("q", destroy1) + b = Button(top, text="Done", command=destroy2) + b.pack(side="bottom") + self._root = top + else: + self._root = root + + # Create some fonts. + self._init_fonts(root) + + # Create the chart canvas. + (self._chart_sb, self._chart_canvas) = self._sb_canvas(self._root) + self._chart_canvas["height"] = 300 + self._chart_canvas["closeenough"] = 15 + + # Create the sentence canvas. + if draw_sentence: + cframe = Frame(self._root, relief="sunk", border=2) + cframe.pack(fill="both", side="bottom") + self._sentence_canvas = Canvas(cframe, height=50) + self._sentence_canvas["background"] = "#e0e0e0" + self._sentence_canvas.pack(fill="both") + # self._sentence_canvas['height'] = self._sentence_height + else: + self._sentence_canvas = None + + # Create the tree canvas. + if draw_tree: + (sb, canvas) = self._sb_canvas(self._root, "n", "x") + (self._tree_sb, self._tree_canvas) = (sb, canvas) + self._tree_canvas["height"] = 200 + else: + self._tree_canvas = None + + # Do some analysis to figure out how big the window should be + self._analyze() + self.draw() + self._resize() + self._grow() + + # Set up the configure callback, which will be called whenever + # the window is resized. + self._chart_canvas.bind("", self._configure) + + def _init_fonts(self, root): + self._boldfont = Font(family="helvetica", weight="bold", size=self._fontsize) + self._font = Font(family="helvetica", size=self._fontsize) + # See: + self._sysfont = Font(font=Button()["font"]) + root.option_add("*Font", self._sysfont) + + def _sb_canvas(self, root, expand="y", fill="both", side="bottom"): + """ + Helper for __init__: construct a canvas with a scrollbar. + """ + cframe = Frame(root, relief="sunk", border=2) + cframe.pack(fill=fill, expand=expand, side=side) + canvas = Canvas(cframe, background="#e0e0e0") + + # Give the canvas a scrollbar. + sb = Scrollbar(cframe, orient="vertical") + sb.pack(side="right", fill="y") + canvas.pack(side="left", fill=fill, expand="yes") + + # Connect the scrollbars to the canvas. + sb["command"] = canvas.yview + canvas["yscrollcommand"] = sb.set + + return (sb, canvas) + + def scroll_up(self, *e): + self._chart_canvas.yview("scroll", -1, "units") + + def scroll_down(self, *e): + self._chart_canvas.yview("scroll", 1, "units") + + def page_up(self, *e): + self._chart_canvas.yview("scroll", -1, "pages") + + def page_down(self, *e): + self._chart_canvas.yview("scroll", 1, "pages") + + def _grow(self): + """ + Grow the window, if necessary + """ + # Grow, if need-be + N = self._chart.num_leaves() + width = max( + int(self._chart_canvas["width"]), N * self._unitsize + ChartView._MARGIN * 2 + ) + + # It won't resize without the second (height) line, but I + # don't understand why not. + self._chart_canvas.configure(width=width) + self._chart_canvas.configure(height=self._chart_canvas["height"]) + + self._unitsize = (width - 2 * ChartView._MARGIN) / N + + # Reset the height for the sentence window. + if self._sentence_canvas is not None: + self._sentence_canvas["height"] = self._sentence_height + + def set_font_size(self, size): + self._font.configure(size=-abs(size)) + self._boldfont.configure(size=-abs(size)) + self._sysfont.configure(size=-abs(size)) + self._analyze() + self._grow() + self.draw() + + def get_font_size(self): + return abs(self._fontsize) + + def _configure(self, e): + """ + The configure callback. This is called whenever the window is + resized. It is also called when the window is first mapped. + It figures out the unit size, and redraws the contents of each + canvas. + """ + N = self._chart.num_leaves() + self._unitsize = (e.width - 2 * ChartView._MARGIN) / N + self.draw() + + def update(self, chart=None): + """ + Draw any edges that have not been drawn. This is typically + called when a after modifies the canvas that a CanvasView is + displaying. ``update`` will cause any edges that have been + added to the chart to be drawn. + + If update is given a ``chart`` argument, then it will replace + the current chart with the given chart. + """ + if chart is not None: + self._chart = chart + self._edgelevels = [] + self._marks = {} + self._analyze() + self._grow() + self.draw() + self.erase_tree() + self._resize() + else: + for edge in self._chart: + if edge not in self._edgetags: + self._add_edge(edge) + self._resize() + + def _edge_conflict(self, edge, lvl): + """ + Return True if the given edge overlaps with any edge on the given + level. This is used by _add_edge to figure out what level a + new edge should be added to. + """ + (s1, e1) = edge.span() + for otheredge in self._edgelevels[lvl]: + (s2, e2) = otheredge.span() + if (s1 <= s2 < e1) or (s2 <= s1 < e2) or (s1 == s2 == e1 == e2): + return True + return False + + def _analyze_edge(self, edge): + """ + Given a new edge, recalculate: + + - _text_height + - _unitsize (if the edge text is too big for the current + _unitsize, then increase _unitsize) + """ + c = self._chart_canvas + + if isinstance(edge, TreeEdge): + lhs = edge.lhs() + rhselts = [] + for elt in edge.rhs(): + if isinstance(elt, Nonterminal): + rhselts.append(str(elt.symbol())) + else: + rhselts.append(repr(elt)) + rhs = " ".join(rhselts) + else: + lhs = edge.lhs() + rhs = "" + + for s in (lhs, rhs): + tag = c.create_text( + 0, 0, text=s, font=self._boldfont, anchor="nw", justify="left" + ) + bbox = c.bbox(tag) + c.delete(tag) + width = bbox[2] # + ChartView._LEAF_SPACING + edgelen = max(edge.length(), 1) + self._unitsize = max(self._unitsize, width / edgelen) + self._text_height = max(self._text_height, bbox[3] - bbox[1]) + + def _add_edge(self, edge, minlvl=0): + """ + Add a single edge to the ChartView: + + - Call analyze_edge to recalculate display parameters + - Find an available level + - Call _draw_edge + """ + # Do NOT show leaf edges in the chart. + if isinstance(edge, LeafEdge): + return + + if edge in self._edgetags: + return + self._analyze_edge(edge) + self._grow() + + if not self._compact: + self._edgelevels.append([edge]) + lvl = len(self._edgelevels) - 1 + self._draw_edge(edge, lvl) + self._resize() + return + + # Figure out what level to draw the edge on. + lvl = 0 + while True: + # If this level doesn't exist yet, create it. + while lvl >= len(self._edgelevels): + self._edgelevels.append([]) + self._resize() + + # Check if we can fit the edge in this level. + if lvl >= minlvl and not self._edge_conflict(edge, lvl): + # Go ahead and draw it. + self._edgelevels[lvl].append(edge) + break + + # Try the next level. + lvl += 1 + + self._draw_edge(edge, lvl) + + def view_edge(self, edge): + level = None + for i in range(len(self._edgelevels)): + if edge in self._edgelevels[i]: + level = i + break + if level is None: + return + # Try to view the new edge.. + y = (level + 1) * self._chart_level_size + dy = self._text_height + 10 + self._chart_canvas.yview("moveto", 1.0) + if self._chart_height != 0: + self._chart_canvas.yview("moveto", (y - dy) / self._chart_height) + + def _draw_edge(self, edge, lvl): + """ + Draw a single edge on the ChartView. + """ + c = self._chart_canvas + + # Draw the arrow. + x1 = edge.start() * self._unitsize + ChartView._MARGIN + x2 = edge.end() * self._unitsize + ChartView._MARGIN + if x2 == x1: + x2 += max(4, self._unitsize / 5) + y = (lvl + 1) * self._chart_level_size + linetag = c.create_line(x1, y, x2, y, arrow="last", width=3) + + # Draw a label for the edge. + if isinstance(edge, TreeEdge): + rhs = [] + for elt in edge.rhs(): + if isinstance(elt, Nonterminal): + rhs.append(str(elt.symbol())) + else: + rhs.append(repr(elt)) + pos = edge.dot() + else: + rhs = [] + pos = 0 + + rhs1 = " ".join(rhs[:pos]) + rhs2 = " ".join(rhs[pos:]) + rhstag1 = c.create_text(x1 + 3, y, text=rhs1, font=self._font, anchor="nw") + dotx = c.bbox(rhstag1)[2] + 6 + doty = (c.bbox(rhstag1)[1] + c.bbox(rhstag1)[3]) / 2 + dottag = c.create_oval(dotx - 2, doty - 2, dotx + 2, doty + 2) + rhstag2 = c.create_text(dotx + 6, y, text=rhs2, font=self._font, anchor="nw") + lhstag = c.create_text( + (x1 + x2) / 2, y, text=str(edge.lhs()), anchor="s", font=self._boldfont + ) + + # Keep track of the edge's tags. + self._edgetags[edge] = (linetag, rhstag1, dottag, rhstag2, lhstag) + + # Register a callback for clicking on the edge. + def cb(event, self=self, edge=edge): + self._fire_callbacks("select", edge) + + c.tag_bind(rhstag1, "", cb) + c.tag_bind(rhstag2, "", cb) + c.tag_bind(linetag, "", cb) + c.tag_bind(dottag, "", cb) + c.tag_bind(lhstag, "", cb) + + self._color_edge(edge) + + def _color_edge(self, edge, linecolor=None, textcolor=None): + """ + Color in an edge with the given colors. + If no colors are specified, use intelligent defaults + (dependent on selection, etc.) + """ + if edge not in self._edgetags: + return + c = self._chart_canvas + + if linecolor is not None and textcolor is not None: + if edge in self._marks: + linecolor = self._marks[edge] + tags = self._edgetags[edge] + c.itemconfig(tags[0], fill=linecolor) + c.itemconfig(tags[1], fill=textcolor) + c.itemconfig(tags[2], fill=textcolor, outline=textcolor) + c.itemconfig(tags[3], fill=textcolor) + c.itemconfig(tags[4], fill=textcolor) + return + else: + N = self._chart.num_leaves() + if edge in self._marks: + self._color_edge(self._marks[edge]) + if edge.is_complete() and edge.span() == (0, N): + self._color_edge(edge, "#084", "#042") + elif isinstance(edge, LeafEdge): + self._color_edge(edge, "#48c", "#246") + else: + self._color_edge(edge, "#00f", "#008") + + def mark_edge(self, edge, mark="#0df"): + """ + Mark an edge + """ + self._marks[edge] = mark + self._color_edge(edge) + + def unmark_edge(self, edge=None): + """ + Unmark an edge (or all edges) + """ + if edge is None: + old_marked_edges = list(self._marks.keys()) + self._marks = {} + for edge in old_marked_edges: + self._color_edge(edge) + else: + del self._marks[edge] + self._color_edge(edge) + + def markonly_edge(self, edge, mark="#0df"): + self.unmark_edge() + self.mark_edge(edge, mark) + + def _analyze(self): + """ + Analyze the sentence string, to figure out how big a unit needs + to be, How big the tree should be, etc. + """ + # Figure out the text height and the unit size. + unitsize = 70 # min unitsize + text_height = 0 + c = self._chart_canvas + + # Check against all tokens + for leaf in self._chart.leaves(): + tag = c.create_text( + 0, 0, text=repr(leaf), font=self._font, anchor="nw", justify="left" + ) + bbox = c.bbox(tag) + c.delete(tag) + width = bbox[2] + ChartView._LEAF_SPACING + unitsize = max(width, unitsize) + text_height = max(text_height, bbox[3] - bbox[1]) + + self._unitsize = unitsize + self._text_height = text_height + self._sentence_height = self._text_height + 2 * ChartView._MARGIN + + # Check against edges. + for edge in self._chart.edges(): + self._analyze_edge(edge) + + # Size of chart levels + self._chart_level_size = self._text_height * 2 + + # Default tree size.. + self._tree_height = 3 * (ChartView._TREE_LEVEL_SIZE + self._text_height) + + # Resize the scrollregions. + self._resize() + + def _resize(self): + """ + Update the scroll-regions for each canvas. This ensures that + everything is within a scroll-region, so the user can use the + scrollbars to view the entire display. This does *not* + resize the window. + """ + c = self._chart_canvas + + # Reset the chart scroll region + width = self._chart.num_leaves() * self._unitsize + ChartView._MARGIN * 2 + + levels = len(self._edgelevels) + self._chart_height = (levels + 2) * self._chart_level_size + c["scrollregion"] = (0, 0, width, self._chart_height) + + # Reset the tree scroll region + if self._tree_canvas: + self._tree_canvas["scrollregion"] = (0, 0, width, self._tree_height) + + def _draw_loclines(self): + """ + Draw location lines. These are vertical gridlines used to + show where each location unit is. + """ + BOTTOM = 50000 + c1 = self._tree_canvas + c2 = self._sentence_canvas + c3 = self._chart_canvas + margin = ChartView._MARGIN + self._loclines = [] + for i in range(0, self._chart.num_leaves() + 1): + x = i * self._unitsize + margin + + if c1: + t1 = c1.create_line(x, 0, x, BOTTOM) + c1.tag_lower(t1) + if c2: + t2 = c2.create_line(x, 0, x, self._sentence_height) + c2.tag_lower(t2) + t3 = c3.create_line(x, 0, x, BOTTOM) + c3.tag_lower(t3) + t4 = c3.create_text(x + 2, 0, text=repr(i), anchor="nw", font=self._font) + c3.tag_lower(t4) + # if i % 4 == 0: + # if c1: c1.itemconfig(t1, width=2, fill='gray60') + # if c2: c2.itemconfig(t2, width=2, fill='gray60') + # c3.itemconfig(t3, width=2, fill='gray60') + if i % 2 == 0: + if c1: + c1.itemconfig(t1, fill="gray60") + if c2: + c2.itemconfig(t2, fill="gray60") + c3.itemconfig(t3, fill="gray60") + else: + if c1: + c1.itemconfig(t1, fill="gray80") + if c2: + c2.itemconfig(t2, fill="gray80") + c3.itemconfig(t3, fill="gray80") + + def _draw_sentence(self): + """Draw the sentence string.""" + if self._chart.num_leaves() == 0: + return + c = self._sentence_canvas + margin = ChartView._MARGIN + y = ChartView._MARGIN + + for i, leaf in enumerate(self._chart.leaves()): + x1 = i * self._unitsize + margin + x2 = x1 + self._unitsize + x = (x1 + x2) / 2 + tag = c.create_text( + x, y, text=repr(leaf), font=self._font, anchor="n", justify="left" + ) + bbox = c.bbox(tag) + rt = c.create_rectangle( + x1 + 2, + bbox[1] - (ChartView._LEAF_SPACING / 2), + x2 - 2, + bbox[3] + (ChartView._LEAF_SPACING / 2), + fill="#f0f0f0", + outline="#f0f0f0", + ) + c.tag_lower(rt) + + def erase_tree(self): + for tag in self._tree_tags: + self._tree_canvas.delete(tag) + self._treetoks = [] + self._treetoks_edge = None + self._treetoks_index = 0 + + def draw_tree(self, edge=None): + if edge is None and self._treetoks_edge is None: + return + if edge is None: + edge = self._treetoks_edge + + # If it's a new edge, then get a new list of treetoks. + if self._treetoks_edge != edge: + self._treetoks = [t for t in self._chart.trees(edge) if isinstance(t, Tree)] + self._treetoks_edge = edge + self._treetoks_index = 0 + + # Make sure there's something to draw. + if len(self._treetoks) == 0: + return + + # Erase the old tree. + for tag in self._tree_tags: + self._tree_canvas.delete(tag) + + # Draw the new tree. + tree = self._treetoks[self._treetoks_index] + self._draw_treetok(tree, edge.start()) + + # Show how many trees are available for the edge. + self._draw_treecycle() + + # Update the scroll region. + w = self._chart.num_leaves() * self._unitsize + 2 * ChartView._MARGIN + h = tree.height() * (ChartView._TREE_LEVEL_SIZE + self._text_height) + self._tree_canvas["scrollregion"] = (0, 0, w, h) + + def cycle_tree(self): + self._treetoks_index = (self._treetoks_index + 1) % len(self._treetoks) + self.draw_tree(self._treetoks_edge) + + def _draw_treecycle(self): + if len(self._treetoks) <= 1: + return + + # Draw the label. + label = "%d Trees" % len(self._treetoks) + c = self._tree_canvas + margin = ChartView._MARGIN + right = self._chart.num_leaves() * self._unitsize + margin - 2 + tag = c.create_text(right, 2, anchor="ne", text=label, font=self._boldfont) + self._tree_tags.append(tag) + _, _, _, y = c.bbox(tag) + + # Draw the triangles. + for i in range(len(self._treetoks)): + x = right - 20 * (len(self._treetoks) - i - 1) + if i == self._treetoks_index: + fill = "#084" + else: + fill = "#fff" + tag = c.create_polygon( + x, y + 10, x - 5, y, x - 10, y + 10, fill=fill, outline="black" + ) + self._tree_tags.append(tag) + + # Set up a callback: show the tree if they click on its + # triangle. + def cb(event, self=self, i=i): + self._treetoks_index = i + self.draw_tree() + + c.tag_bind(tag, "", cb) + + def _draw_treetok(self, treetok, index, depth=0): + """ + :param index: The index of the first leaf in the tree. + :return: The index of the first leaf after the tree. + """ + c = self._tree_canvas + margin = ChartView._MARGIN + + # Draw the children + child_xs = [] + for child in treetok: + if isinstance(child, Tree): + child_x, index = self._draw_treetok(child, index, depth + 1) + child_xs.append(child_x) + else: + child_xs.append((2 * index + 1) * self._unitsize / 2 + margin) + index += 1 + + # If we have children, then get the node's x by averaging their + # node x's. Otherwise, make room for ourselves. + if child_xs: + nodex = sum(child_xs) / len(child_xs) + else: + # [XX] breaks for null productions. + nodex = (2 * index + 1) * self._unitsize / 2 + margin + index += 1 + + # Draw the node + nodey = depth * (ChartView._TREE_LEVEL_SIZE + self._text_height) + tag = c.create_text( + nodex, + nodey, + anchor="n", + justify="center", + text=str(treetok.label()), + fill="#042", + font=self._boldfont, + ) + self._tree_tags.append(tag) + + # Draw lines to the children. + childy = nodey + ChartView._TREE_LEVEL_SIZE + self._text_height + for childx, child in zip(child_xs, treetok): + if isinstance(child, Tree) and child: + # A "real" tree token: + tag = c.create_line( + nodex, + nodey + self._text_height, + childx, + childy, + width=2, + fill="#084", + ) + self._tree_tags.append(tag) + if isinstance(child, Tree) and not child: + # An unexpanded tree token: + tag = c.create_line( + nodex, + nodey + self._text_height, + childx, + childy, + width=2, + fill="#048", + dash="2 3", + ) + self._tree_tags.append(tag) + if not isinstance(child, Tree): + # A leaf: + tag = c.create_line( + nodex, + nodey + self._text_height, + childx, + 10000, + width=2, + fill="#084", + ) + self._tree_tags.append(tag) + + return nodex, index + + def draw(self): + """ + Draw everything (from scratch). + """ + if self._tree_canvas: + self._tree_canvas.delete("all") + self.draw_tree() + + if self._sentence_canvas: + self._sentence_canvas.delete("all") + self._draw_sentence() + + self._chart_canvas.delete("all") + self._edgetags = {} + + # Redraw any edges we erased. + for lvl in range(len(self._edgelevels)): + for edge in self._edgelevels[lvl]: + self._draw_edge(edge, lvl) + + for edge in self._chart: + self._add_edge(edge) + + self._draw_loclines() + + def add_callback(self, event, func): + self._callbacks.setdefault(event, {})[func] = 1 + + def remove_callback(self, event, func=None): + if func is None: + del self._callbacks[event] + else: + try: + del self._callbacks[event][func] + except KeyError: + pass + + def _fire_callbacks(self, event, *args): + if event not in self._callbacks: + return + for cb_func in list(self._callbacks[event].keys()): + cb_func(*args) + + +####################################################################### +# Edge Rules +####################################################################### +# These version of the chart rules only apply to a specific edge. +# This lets the user select an edge, and then apply a rule. + + +class EdgeRule: + """ + To create an edge rule, make an empty base class that uses + EdgeRule as the first base class, and the basic rule as the + second base class. (Order matters!) + """ + + def __init__(self, edge): + super = self.__class__.__bases__[1] + self._edge = edge + self.NUM_EDGES = super.NUM_EDGES - 1 + + def apply(self, chart, grammar, *edges): + super = self.__class__.__bases__[1] + edges += (self._edge,) + yield from super.apply(self, chart, grammar, *edges) + + def __str__(self): + super = self.__class__.__bases__[1] + return super.__str__(self) + + +class TopDownPredictEdgeRule(EdgeRule, TopDownPredictRule): + pass + + +class BottomUpEdgeRule(EdgeRule, BottomUpPredictRule): + pass + + +class BottomUpLeftCornerEdgeRule(EdgeRule, BottomUpPredictCombineRule): + pass + + +class FundamentalEdgeRule(EdgeRule, SingleEdgeFundamentalRule): + pass + + +####################################################################### +# Chart Parser Application +####################################################################### + + +class ChartParserApp: + def __init__(self, grammar, tokens, title="Chart Parser Application"): + # Initialize the parser + self._init_parser(grammar, tokens) + + self._root = None + try: + # Create the root window. + self._root = Tk() + self._root.title(title) + self._root.bind("", self.destroy) + + # Set up some frames. + frame3 = Frame(self._root) + frame2 = Frame(self._root) + frame1 = Frame(self._root) + frame3.pack(side="bottom", fill="none") + frame2.pack(side="bottom", fill="x") + frame1.pack(side="bottom", fill="both", expand=1) + + self._init_fonts(self._root) + self._init_animation() + self._init_chartview(frame1) + self._init_rulelabel(frame2) + self._init_buttons(frame3) + self._init_menubar() + + self._matrix = None + self._results = None + + # Set up keyboard bindings. + self._init_bindings() + + except Exception: + print("Error creating Tree View") + self.destroy() + raise + + def destroy(self, *args): + if self._root is None: + return + self._root.destroy() + self._root = None + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this demo is created from a non-interactive program (e.g. + from a secript); otherwise, the demo will close as soon as + the script completes. + """ + if in_idle(): + return + self._root.mainloop(*args, **kwargs) + + # //////////////////////////////////////////////////////////// + # Initialization Helpers + # //////////////////////////////////////////////////////////// + + def _init_parser(self, grammar, tokens): + self._grammar = grammar + self._tokens = tokens + self._reset_parser() + + def _reset_parser(self): + self._cp = SteppingChartParser(self._grammar) + self._cp.initialize(self._tokens) + self._chart = self._cp.chart() + + # Insert LeafEdges before the parsing starts. + for _new_edge in LeafInitRule().apply(self._chart, self._grammar): + pass + + # The step iterator -- use this to generate new edges + self._cpstep = self._cp.step() + + # The currently selected edge + self._selection = None + + def _init_fonts(self, root): + # See: + self._sysfont = Font(font=Button()["font"]) + root.option_add("*Font", self._sysfont) + + # TWhat's our font size (default=same as sysfont) + self._size = IntVar(root) + self._size.set(self._sysfont.cget("size")) + + self._boldfont = Font(family="helvetica", weight="bold", size=self._size.get()) + self._font = Font(family="helvetica", size=self._size.get()) + + def _init_animation(self): + # Are we stepping? (default=yes) + self._step = IntVar(self._root) + self._step.set(1) + + # What's our animation speed (default=fast) + self._animate = IntVar(self._root) + self._animate.set(3) # Default speed = fast + + # Are we currently animating? + self._animating = 0 + + def _init_chartview(self, parent): + self._cv = ChartView(self._chart, parent, draw_tree=1, draw_sentence=1) + self._cv.add_callback("select", self._click_cv_edge) + + def _init_rulelabel(self, parent): + ruletxt = "Last edge generated by:" + + self._rulelabel1 = Label(parent, text=ruletxt, font=self._boldfont) + self._rulelabel2 = Label( + parent, width=40, relief="groove", anchor="w", font=self._boldfont + ) + self._rulelabel1.pack(side="left") + self._rulelabel2.pack(side="left") + step = Checkbutton(parent, variable=self._step, text="Step") + step.pack(side="right") + + def _init_buttons(self, parent): + frame1 = Frame(parent) + frame2 = Frame(parent) + frame1.pack(side="bottom", fill="x") + frame2.pack(side="top", fill="none") + + Button( + frame1, + text="Reset\nParser", + background="#90c0d0", + foreground="black", + command=self.reset, + ).pack(side="right") + # Button(frame1, text='Pause', + # background='#90c0d0', foreground='black', + # command=self.pause).pack(side='left') + + Button( + frame1, + text="Top Down\nStrategy", + background="#90c0d0", + foreground="black", + command=self.top_down_strategy, + ).pack(side="left") + Button( + frame1, + text="Bottom Up\nStrategy", + background="#90c0d0", + foreground="black", + command=self.bottom_up_strategy, + ).pack(side="left") + Button( + frame1, + text="Bottom Up\nLeft-Corner Strategy", + background="#90c0d0", + foreground="black", + command=self.bottom_up_leftcorner_strategy, + ).pack(side="left") + + Button( + frame2, + text="Top Down Init\nRule", + background="#90f090", + foreground="black", + command=self.top_down_init, + ).pack(side="left") + Button( + frame2, + text="Top Down Predict\nRule", + background="#90f090", + foreground="black", + command=self.top_down_predict, + ).pack(side="left") + Frame(frame2, width=20).pack(side="left") + + Button( + frame2, + text="Bottom Up Predict\nRule", + background="#90f090", + foreground="black", + command=self.bottom_up, + ).pack(side="left") + Frame(frame2, width=20).pack(side="left") + + Button( + frame2, + text="Bottom Up Left-Corner\nPredict Rule", + background="#90f090", + foreground="black", + command=self.bottom_up_leftcorner, + ).pack(side="left") + Frame(frame2, width=20).pack(side="left") + + Button( + frame2, + text="Fundamental\nRule", + background="#90f090", + foreground="black", + command=self.fundamental, + ).pack(side="left") + + def _init_bindings(self): + self._root.bind("", self._cv.scroll_up) + self._root.bind("", self._cv.scroll_down) + self._root.bind("", self._cv.page_up) + self._root.bind("", self._cv.page_down) + self._root.bind("", self.destroy) + self._root.bind("", self.destroy) + self._root.bind("", self.help) + + self._root.bind("", self.save_chart) + self._root.bind("", self.load_chart) + self._root.bind("", self.reset) + + self._root.bind("t", self.top_down_strategy) + self._root.bind("b", self.bottom_up_strategy) + self._root.bind("c", self.bottom_up_leftcorner_strategy) + self._root.bind("", self._stop_animation) + + self._root.bind("", self.edit_grammar) + self._root.bind("", self.edit_sentence) + + # Animation speed control + self._root.bind("-", lambda e, a=self._animate: a.set(1)) + self._root.bind("=", lambda e, a=self._animate: a.set(2)) + self._root.bind("+", lambda e, a=self._animate: a.set(3)) + + # Step control + self._root.bind("s", lambda e, s=self._step: s.set(not s.get())) + + def _init_menubar(self): + menubar = Menu(self._root) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Save Chart", + underline=0, + command=self.save_chart, + accelerator="Ctrl-s", + ) + filemenu.add_command( + label="Load Chart", + underline=0, + command=self.load_chart, + accelerator="Ctrl-o", + ) + filemenu.add_command( + label="Reset Chart", underline=0, command=self.reset, accelerator="Ctrl-r" + ) + filemenu.add_separator() + filemenu.add_command(label="Save Grammar", command=self.save_grammar) + filemenu.add_command(label="Load Grammar", command=self.load_grammar) + filemenu.add_separator() + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + editmenu = Menu(menubar, tearoff=0) + editmenu.add_command( + label="Edit Grammar", + underline=5, + command=self.edit_grammar, + accelerator="Ctrl-g", + ) + editmenu.add_command( + label="Edit Text", + underline=5, + command=self.edit_sentence, + accelerator="Ctrl-t", + ) + menubar.add_cascade(label="Edit", underline=0, menu=editmenu) + + viewmenu = Menu(menubar, tearoff=0) + viewmenu.add_command( + label="Chart Matrix", underline=6, command=self.view_matrix + ) + viewmenu.add_command(label="Results", underline=0, command=self.view_results) + menubar.add_cascade(label="View", underline=0, menu=viewmenu) + + rulemenu = Menu(menubar, tearoff=0) + rulemenu.add_command( + label="Top Down Strategy", + underline=0, + command=self.top_down_strategy, + accelerator="t", + ) + rulemenu.add_command( + label="Bottom Up Strategy", + underline=0, + command=self.bottom_up_strategy, + accelerator="b", + ) + rulemenu.add_command( + label="Bottom Up Left-Corner Strategy", + underline=0, + command=self.bottom_up_leftcorner_strategy, + accelerator="c", + ) + rulemenu.add_separator() + rulemenu.add_command(label="Bottom Up Rule", command=self.bottom_up) + rulemenu.add_command( + label="Bottom Up Left-Corner Rule", command=self.bottom_up_leftcorner + ) + rulemenu.add_command(label="Top Down Init Rule", command=self.top_down_init) + rulemenu.add_command( + label="Top Down Predict Rule", command=self.top_down_predict + ) + rulemenu.add_command(label="Fundamental Rule", command=self.fundamental) + menubar.add_cascade(label="Apply", underline=0, menu=rulemenu) + + animatemenu = Menu(menubar, tearoff=0) + animatemenu.add_checkbutton( + label="Step", underline=0, variable=self._step, accelerator="s" + ) + animatemenu.add_separator() + animatemenu.add_radiobutton( + label="No Animation", underline=0, variable=self._animate, value=0 + ) + animatemenu.add_radiobutton( + label="Slow Animation", + underline=0, + variable=self._animate, + value=1, + accelerator="-", + ) + animatemenu.add_radiobutton( + label="Normal Animation", + underline=0, + variable=self._animate, + value=2, + accelerator="=", + ) + animatemenu.add_radiobutton( + label="Fast Animation", + underline=0, + variable=self._animate, + value=3, + accelerator="+", + ) + menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) + + zoommenu = Menu(menubar, tearoff=0) + zoommenu.add_radiobutton( + label="Tiny", + variable=self._size, + underline=0, + value=10, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Small", + variable=self._size, + underline=0, + value=12, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Medium", + variable=self._size, + underline=0, + value=14, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Large", + variable=self._size, + underline=0, + value=18, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Huge", + variable=self._size, + underline=0, + value=24, + command=self.resize, + ) + menubar.add_cascade(label="Zoom", underline=0, menu=zoommenu) + + helpmenu = Menu(menubar, tearoff=0) + helpmenu.add_command(label="About", underline=0, command=self.about) + helpmenu.add_command( + label="Instructions", underline=0, command=self.help, accelerator="F1" + ) + menubar.add_cascade(label="Help", underline=0, menu=helpmenu) + + self._root.config(menu=menubar) + + # //////////////////////////////////////////////////////////// + # Selection Handling + # //////////////////////////////////////////////////////////// + + def _click_cv_edge(self, edge): + if edge != self._selection: + # Clicking on a new edge selects it. + self._select_edge(edge) + else: + # Repeated clicks on one edge cycle its trees. + self._cv.cycle_tree() + # [XX] this can get confused if animation is running + # faster than the callbacks... + + def _select_matrix_edge(self, edge): + self._select_edge(edge) + self._cv.view_edge(edge) + + def _select_edge(self, edge): + self._selection = edge + # Update the chart view. + self._cv.markonly_edge(edge, "#f00") + self._cv.draw_tree(edge) + # Update the matrix view. + if self._matrix: + self._matrix.markonly_edge(edge) + if self._matrix: + self._matrix.view_edge(edge) + + def _deselect_edge(self): + self._selection = None + # Update the chart view. + self._cv.unmark_edge() + self._cv.erase_tree() + # Update the matrix view + if self._matrix: + self._matrix.unmark_edge() + + def _show_new_edge(self, edge): + self._display_rule(self._cp.current_chartrule()) + # Update the chart view. + self._cv.update() + self._cv.draw_tree(edge) + self._cv.markonly_edge(edge, "#0df") + self._cv.view_edge(edge) + # Update the matrix view. + if self._matrix: + self._matrix.update() + if self._matrix: + self._matrix.markonly_edge(edge) + if self._matrix: + self._matrix.view_edge(edge) + # Update the results view. + if self._results: + self._results.update(edge) + + # //////////////////////////////////////////////////////////// + # Help/usage + # //////////////////////////////////////////////////////////// + + def help(self, *e): + self._animating = 0 + # The default font's not very legible; try using 'fixed' instead. + try: + ShowText( + self._root, + "Help: Chart Parser Application", + (__doc__ or "").strip(), + width=75, + font="fixed", + ) + except Exception: + ShowText( + self._root, + "Help: Chart Parser Application", + (__doc__ or "").strip(), + width=75, + ) + + def about(self, *e): + ABOUT = "NLTK Chart Parser Application\n" + "Written by Edward Loper" + showinfo("About: Chart Parser Application", ABOUT) + + # //////////////////////////////////////////////////////////// + # File Menu + # //////////////////////////////////////////////////////////// + + CHART_FILE_TYPES = [("Pickle file", ".pickle"), ("All files", "*")] + GRAMMAR_FILE_TYPES = [ + ("Plaintext grammar file", ".cfg"), + ("Pickle file", ".pickle"), + ("All files", "*"), + ] + + def load_chart(self, *args): + "Load a chart from a pickle file" + filename = askopenfilename( + filetypes=self.CHART_FILE_TYPES, defaultextension=".pickle" + ) + if not filename: + return + try: + with open(filename, "rb") as infile: + chart = pickle_load(infile) + self._chart = chart + self._cv.update(chart) + if self._matrix: + self._matrix.set_chart(chart) + if self._matrix: + self._matrix.deselect_cell() + if self._results: + self._results.set_chart(chart) + self._cp.set_chart(chart) + except Exception as e: + raise + showerror("Error Loading Chart", "Unable to open file: %r" % filename) + + def save_chart(self, *args): + "Save a chart to a pickle file" + filename = asksaveasfilename( + filetypes=self.CHART_FILE_TYPES, defaultextension=".pickle" + ) + if not filename: + return + try: + with open(filename, "wb") as outfile: + pickle.dump(self._chart, outfile) + except Exception as e: + raise + showerror("Error Saving Chart", "Unable to open file: %r" % filename) + + def load_grammar(self, *args): + "Load a grammar from a pickle file" + filename = askopenfilename( + filetypes=self.GRAMMAR_FILE_TYPES, defaultextension=".cfg" + ) + if not filename: + return + try: + if filename.endswith(".pickle"): + with open(filename, "rb") as infile: + grammar = pickle_load(infile) + else: + with open(filename) as infile: + grammar = CFG.fromstring(infile.read()) + self.set_grammar(grammar) + except Exception as e: + showerror("Error Loading Grammar", "Unable to open file: %r" % filename) + + def save_grammar(self, *args): + filename = asksaveasfilename( + filetypes=self.GRAMMAR_FILE_TYPES, defaultextension=".cfg" + ) + if not filename: + return + try: + if filename.endswith(".pickle"): + with open(filename, "wb") as outfile: + pickle.dump((self._chart, self._tokens), outfile) + else: + with open(filename, "w") as outfile: + prods = self._grammar.productions() + start = [p for p in prods if p.lhs() == self._grammar.start()] + rest = [p for p in prods if p.lhs() != self._grammar.start()] + for prod in start: + outfile.write("%s\n" % prod) + for prod in rest: + outfile.write("%s\n" % prod) + except Exception as e: + showerror("Error Saving Grammar", "Unable to open file: %r" % filename) + + def reset(self, *args): + self._animating = 0 + self._reset_parser() + self._cv.update(self._chart) + if self._matrix: + self._matrix.set_chart(self._chart) + if self._matrix: + self._matrix.deselect_cell() + if self._results: + self._results.set_chart(self._chart) + + # //////////////////////////////////////////////////////////// + # Edit + # //////////////////////////////////////////////////////////// + + def edit_grammar(self, *e): + CFGEditor(self._root, self._grammar, self.set_grammar) + + def set_grammar(self, grammar): + self._grammar = grammar + self._cp.set_grammar(grammar) + if self._results: + self._results.set_grammar(grammar) + + def edit_sentence(self, *e): + sentence = " ".join(self._tokens) + title = "Edit Text" + instr = "Enter a new sentence to parse." + EntryDialog(self._root, sentence, instr, self.set_sentence, title) + + def set_sentence(self, sentence): + self._tokens = list(sentence.split()) + self.reset() + + # //////////////////////////////////////////////////////////// + # View Menu + # //////////////////////////////////////////////////////////// + + def view_matrix(self, *e): + if self._matrix is not None: + self._matrix.destroy() + self._matrix = ChartMatrixView(self._root, self._chart) + self._matrix.add_callback("select", self._select_matrix_edge) + + def view_results(self, *e): + if self._results is not None: + self._results.destroy() + self._results = ChartResultsView(self._root, self._chart, self._grammar) + + # //////////////////////////////////////////////////////////// + # Zoom Menu + # //////////////////////////////////////////////////////////// + + def resize(self): + self._animating = 0 + self.set_font_size(self._size.get()) + + def set_font_size(self, size): + self._cv.set_font_size(size) + self._font.configure(size=-abs(size)) + self._boldfont.configure(size=-abs(size)) + self._sysfont.configure(size=-abs(size)) + + def get_font_size(self): + return abs(self._size.get()) + + # //////////////////////////////////////////////////////////// + # Parsing + # //////////////////////////////////////////////////////////// + + def apply_strategy(self, strategy, edge_strategy=None): + # If we're animating, then stop. + if self._animating: + self._animating = 0 + return + + # Clear the rule display & mark. + self._display_rule(None) + # self._cv.unmark_edge() + + if self._step.get(): + selection = self._selection + if (selection is not None) and (edge_strategy is not None): + # Apply the given strategy to the selected edge. + self._cp.set_strategy([edge_strategy(selection)]) + newedge = self._apply_strategy() + + # If it failed, then clear the selection. + if newedge is None: + self._cv.unmark_edge() + self._selection = None + else: + self._cp.set_strategy(strategy) + self._apply_strategy() + + else: + self._cp.set_strategy(strategy) + if self._animate.get(): + self._animating = 1 + self._animate_strategy() + else: + for edge in self._cpstep: + if edge is None: + break + self._cv.update() + if self._matrix: + self._matrix.update() + if self._results: + self._results.update() + + def _stop_animation(self, *e): + self._animating = 0 + + def _animate_strategy(self, speed=1): + if self._animating == 0: + return + if self._apply_strategy() is not None: + if self._animate.get() == 0 or self._step.get() == 1: + return + if self._animate.get() == 1: + self._root.after(3000, self._animate_strategy) + elif self._animate.get() == 2: + self._root.after(1000, self._animate_strategy) + else: + self._root.after(20, self._animate_strategy) + + def _apply_strategy(self): + new_edge = next(self._cpstep) + + if new_edge is not None: + self._show_new_edge(new_edge) + return new_edge + + def _display_rule(self, rule): + if rule is None: + self._rulelabel2["text"] = "" + else: + name = str(rule) + self._rulelabel2["text"] = name + size = self._cv.get_font_size() + + # //////////////////////////////////////////////////////////// + # Parsing Strategies + # //////////////////////////////////////////////////////////// + + # Basic rules: + _TD_INIT = [TopDownInitRule()] + _TD_PREDICT = [TopDownPredictRule()] + _BU_RULE = [BottomUpPredictRule()] + _BU_LC_RULE = [BottomUpPredictCombineRule()] + _FUNDAMENTAL = [SingleEdgeFundamentalRule()] + + # Complete strategies: + _TD_STRATEGY = _TD_INIT + _TD_PREDICT + _FUNDAMENTAL + _BU_STRATEGY = _BU_RULE + _FUNDAMENTAL + _BU_LC_STRATEGY = _BU_LC_RULE + _FUNDAMENTAL + + # Button callback functions: + def top_down_init(self, *e): + self.apply_strategy(self._TD_INIT, None) + + def top_down_predict(self, *e): + self.apply_strategy(self._TD_PREDICT, TopDownPredictEdgeRule) + + def bottom_up(self, *e): + self.apply_strategy(self._BU_RULE, BottomUpEdgeRule) + + def bottom_up_leftcorner(self, *e): + self.apply_strategy(self._BU_LC_RULE, BottomUpLeftCornerEdgeRule) + + def fundamental(self, *e): + self.apply_strategy(self._FUNDAMENTAL, FundamentalEdgeRule) + + def bottom_up_strategy(self, *e): + self.apply_strategy(self._BU_STRATEGY, BottomUpEdgeRule) + + def bottom_up_leftcorner_strategy(self, *e): + self.apply_strategy(self._BU_LC_STRATEGY, BottomUpLeftCornerEdgeRule) + + def top_down_strategy(self, *e): + self.apply_strategy(self._TD_STRATEGY, TopDownPredictEdgeRule) + + +def app(): + grammar = CFG.fromstring( + """ + # Grammatical productions. + S -> NP VP + VP -> VP PP | V NP | V + NP -> Det N | NP PP + PP -> P NP + # Lexical productions. + NP -> 'John' | 'I' + Det -> 'the' | 'my' | 'a' + N -> 'dog' | 'cookie' | 'table' | 'cake' | 'fork' + V -> 'ate' | 'saw' + P -> 'on' | 'under' | 'with' + """ + ) + + sent = "John ate the cake on the table with a fork" + sent = "John ate the cake on the table" + tokens = list(sent.split()) + + print("grammar= (") + for rule in grammar.productions(): + print((" ", repr(rule) + ",")) + print(")") + print("tokens = %r" % tokens) + print('Calling "ChartParserApp(grammar, tokens)"...') + ChartParserApp(grammar, tokens).mainloop() + + +if __name__ == "__main__": + app() + + # Chart comparer: + # charts = ['/tmp/earley.pickle', + # '/tmp/topdown.pickle', + # '/tmp/bottomup.pickle'] + # ChartComparer(*charts).mainloop() + + # import profile + # profile.run('demo2()', '/tmp/profile.out') + # import pstats + # p = pstats.Stats('/tmp/profile.out') + # p.strip_dirs().sort_stats('time', 'cum').print_stats(60) + # p.strip_dirs().sort_stats('cum', 'time').print_stats(60) + +__all__ = ["app"] diff --git a/nltk/app/chunkparser_app.py b/nltk/app/chunkparser_app.py new file mode 100644 index 0000000..4f221f8 --- /dev/null +++ b/nltk/app/chunkparser_app.py @@ -0,0 +1,1500 @@ +# Natural Language Toolkit: Regexp Chunk Parser Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A graphical tool for exploring the regular expression based chunk +parser ``nltk.chunk.RegexpChunkParser``. +""" + +# Todo: Add a way to select the development set from the menubar. This +# might just need to be a selection box (conll vs treebank etc) plus +# configuration parameters to select what's being chunked (eg VP vs NP) +# and what part of the data is being used as the development set. + +import random +import re +import textwrap +import time +from tkinter import ( + Button, + Canvas, + Checkbutton, + Frame, + IntVar, + Label, + Menu, + Scrollbar, + Text, + Tk, +) +from tkinter.filedialog import askopenfilename, asksaveasfilename +from tkinter.font import Font + +from nltk.chunk import ChunkScore, RegexpChunkParser +from nltk.chunk.regexp import RegexpChunkRule +from nltk.corpus import conll2000, treebank_chunk +from nltk.draw.util import ShowText +from nltk.tree import Tree +from nltk.util import in_idle + + +class RegexpChunkApp: + """ + A graphical tool for exploring the regular expression based chunk + parser ``nltk.chunk.RegexpChunkParser``. + + See ``HELP`` for instructional text. + """ + + ##///////////////////////////////////////////////////////////////// + ## Help Text + ##///////////////////////////////////////////////////////////////// + + #: A dictionary mapping from part of speech tags to descriptions, + #: which is used in the help text. (This should probably live with + #: the conll and/or treebank corpus instead.) + TAGSET = { + "CC": "Coordinating conjunction", + "PRP$": "Possessive pronoun", + "CD": "Cardinal number", + "RB": "Adverb", + "DT": "Determiner", + "RBR": "Adverb, comparative", + "EX": "Existential there", + "RBS": "Adverb, superlative", + "FW": "Foreign word", + "RP": "Particle", + "JJ": "Adjective", + "TO": "to", + "JJR": "Adjective, comparative", + "UH": "Interjection", + "JJS": "Adjective, superlative", + "VB": "Verb, base form", + "LS": "List item marker", + "VBD": "Verb, past tense", + "MD": "Modal", + "NNS": "Noun, plural", + "NN": "Noun, singular or mass", + "VBN": "Verb, past participle", + "VBZ": "Verb,3rd ps. sing. present", + "NNP": "Proper noun, singular", + "NNPS": "Proper noun plural", + "WDT": "wh-determiner", + "PDT": "Predeterminer", + "WP": "wh-pronoun", + "POS": "Possessive ending", + "WP$": "Possessive wh-pronoun", + "PRP": "Personal pronoun", + "WRB": "wh-adverb", + "(": "open parenthesis", + ")": "close parenthesis", + "``": "open quote", + ",": "comma", + "''": "close quote", + ".": "period", + "#": "pound sign (currency marker)", + "$": "dollar sign (currency marker)", + "IN": "Preposition/subord. conjunction", + "SYM": "Symbol (mathematical or scientific)", + "VBG": "Verb, gerund/present participle", + "VBP": "Verb, non-3rd ps. sing. present", + ":": "colon", + } + + #: Contents for the help box. This is a list of tuples, one for + #: each help page, where each tuple has four elements: + #: - A title (displayed as a tab) + #: - A string description of tabstops (see Tkinter.Text for details) + #: - The text contents for the help page. You can use expressions + #: like ... to colorize the text; see ``HELP_AUTOTAG`` + #: for a list of tags you can use for colorizing. + HELP = [ + ( + "Help", + "20", + "Welcome to the regular expression chunk-parser grammar editor. " + "You can use this editor to develop and test chunk parser grammars " + "based on NLTK's RegexpChunkParser class.\n\n" + # Help box. + "Use this box ('Help') to learn more about the editor; click on the " + "tabs for help on specific topics:" + "\n" + "Rules: grammar rule types\n" + "Regexps: regular expression syntax\n" + "Tags: part of speech tags\n\n" + # Grammar. + "Use the upper-left box ('Grammar') to edit your grammar. " + "Each line of your grammar specifies a single 'rule', " + "which performs an action such as creating a chunk or merging " + "two chunks.\n\n" + # Dev set. + "The lower-left box ('Development Set') runs your grammar on the " + "development set, and displays the results. " + "Your grammar's chunks are highlighted, and " + "the correct (gold standard) chunks are " + "underlined. If they " + "match, they are displayed in green; otherwise, " + "they are displayed in red. The box displays a single " + "sentence from the development set at a time; use the scrollbar or " + "the next/previous buttons view additional sentences.\n\n" + # Performance + "The lower-right box ('Evaluation') tracks the performance of " + "your grammar on the development set. The 'precision' axis " + "indicates how many of your grammar's chunks are correct; and " + "the 'recall' axis indicates how many of the gold standard " + "chunks your system generated. Typically, you should try to " + "design a grammar that scores high on both metrics. The " + "exact precision and recall of the current grammar, as well " + "as their harmonic mean (the 'f-score'), are displayed in " + "the status bar at the bottom of the window.", + ), + ( + "Rules", + "10", + "

{...regexp...}

" + "\nChunk rule: creates new chunks from words matching " + "regexp.\n\n" + "

}...regexp...{

" + "\nStrip rule: removes words matching regexp from existing " + "chunks.\n\n" + "

...regexp1...}{...regexp2...

" + "\nSplit rule: splits chunks that match regexp1 followed by " + "regexp2 in two.\n\n" + "

...regexp...{}...regexp...

" + "\nMerge rule: joins consecutive chunks that match regexp1 " + "and regexp2\n", + ), + ( + "Regexps", + "10 60", + # "Regular Expression Syntax Summary:\n\n" + "

Pattern\t\tMatches...

\n" + "" + "\t<T>\ta word with tag T " + "(where T may be a regexp).\n" + "\tx?\tan optional x\n" + "\tx+\ta sequence of 1 or more x's\n" + "\tx*\ta sequence of 0 or more x's\n" + "\tx|y\tx or y\n" + "\t.\tmatches any character\n" + "\t(x)\tTreats x as a group\n" + "\t# x...\tTreats x... " + "(to the end of the line) as a comment\n" + "\t\\C\tmatches character C " + "(useful when C is a special character " + "like + or #)\n" + "" + "\n

Examples:

\n" + "" + "\t\n" + '\t\tMatches "cow/NN"\n' + '\t\tMatches "green/NN"\n' + "\t\n" + '\t\tMatches "eating/VBG"\n' + '\t\tMatches "ate/VBD"\n' + "\t
\n" + '\t\tMatches "on/IN the/DT car/NN"\n' + "\t?\n" + '\t\tMatches "ran/VBD"\n' + '\t\tMatches "slowly/RB ate/VBD"\n' + r"\t<\#> # This is a comment...\n" + '\t\tMatches "#/# 100/CD"\n' + "", + ), + ( + "Tags", + "10 60", + "

Part of Speech Tags:

\n" + + "" + + "<>" + + "\n", # this gets auto-substituted w/ self.TAGSET + ), + ] + + HELP_AUTOTAG = [ + ("red", dict(foreground="#a00")), + ("green", dict(foreground="#080")), + ("highlight", dict(background="#ddd")), + ("underline", dict(underline=True)), + ("h1", dict(underline=True)), + ("indent", dict(lmargin1=20, lmargin2=20)), + ("hangindent", dict(lmargin1=0, lmargin2=60)), + ("var", dict(foreground="#88f")), + ("regexp", dict(foreground="#ba7")), + ("match", dict(foreground="#6a6")), + ] + + ##///////////////////////////////////////////////////////////////// + ## Config Parameters + ##///////////////////////////////////////////////////////////////// + + _EVAL_DELAY = 1 + """If the user has not pressed any key for this amount of time (in + seconds), and the current grammar has not been evaluated, then + the eval demon will evaluate it.""" + + _EVAL_CHUNK = 15 + """The number of sentences that should be evaluated by the eval + demon each time it runs.""" + _EVAL_FREQ = 0.2 + """The frequency (in seconds) at which the eval demon is run""" + _EVAL_DEMON_MIN = 0.02 + """The minimum amount of time that the eval demon should take each time + it runs -- if it takes less than this time, _EVAL_CHUNK will be + modified upwards.""" + _EVAL_DEMON_MAX = 0.04 + """The maximum amount of time that the eval demon should take each time + it runs -- if it takes more than this time, _EVAL_CHUNK will be + modified downwards.""" + + _GRAMMARBOX_PARAMS = dict( + width=40, + height=12, + background="#efe", + highlightbackground="#efe", + highlightthickness=1, + relief="groove", + border=2, + wrap="word", + ) + _HELPBOX_PARAMS = dict( + width=15, + height=15, + background="#efe", + highlightbackground="#efe", + foreground="#555", + highlightthickness=1, + relief="groove", + border=2, + wrap="word", + ) + _DEVSETBOX_PARAMS = dict( + width=70, + height=10, + background="#eef", + highlightbackground="#eef", + highlightthickness=1, + relief="groove", + border=2, + wrap="word", + tabs=(30,), + ) + _STATUS_PARAMS = dict(background="#9bb", relief="groove", border=2) + _FONT_PARAMS = dict(family="helvetica", size=-20) + _FRAME_PARAMS = dict(background="#777", padx=2, pady=2, border=3) + _EVALBOX_PARAMS = dict( + background="#eef", + highlightbackground="#eef", + highlightthickness=1, + relief="groove", + border=2, + width=300, + height=280, + ) + _BUTTON_PARAMS = dict( + background="#777", activebackground="#777", highlightbackground="#777" + ) + _HELPTAB_BG_COLOR = "#aba" + _HELPTAB_FG_COLOR = "#efe" + + _HELPTAB_FG_PARAMS = dict(background="#efe") + _HELPTAB_BG_PARAMS = dict(background="#aba") + _HELPTAB_SPACER = 6 + + def normalize_grammar(self, grammar): + # Strip comments + grammar = re.sub(r"((\\.|[^#])*)(#.*)?", r"\1", grammar) + # Normalize whitespace + grammar = re.sub(" +", " ", grammar) + grammar = re.sub(r"\n\s+", r"\n", grammar) + grammar = grammar.strip() + # [xx] Hack: automatically backslash $! + grammar = re.sub(r"([^\\])\$", r"\1\\$", grammar) + return grammar + + def __init__( + self, + devset_name="conll2000", + devset=None, + grammar="", + chunk_label="NP", + tagset=None, + ): + """ + :param devset_name: The name of the development set; used for + display & for save files. If either the name 'treebank' + or the name 'conll2000' is used, and devset is None, then + devset will be set automatically. + :param devset: A list of chunked sentences + :param grammar: The initial grammar to display. + :param tagset: Dictionary from tags to string descriptions, used + for the help page. Defaults to ``self.TAGSET``. + """ + self._chunk_label = chunk_label + + if tagset is None: + tagset = self.TAGSET + self.tagset = tagset + + # Named development sets: + if devset is None: + if devset_name == "conll2000": + devset = conll2000.chunked_sents("train.txt") # [:100] + elif devset == "treebank": + devset = treebank_chunk.chunked_sents() # [:100] + else: + raise ValueError("Unknown development set %s" % devset_name) + + self.chunker = None + """The chunker built from the grammar string""" + + self.grammar = grammar + """The unparsed grammar string""" + + self.normalized_grammar = None + """A normalized version of ``self.grammar``.""" + + self.grammar_changed = 0 + """The last time() that the grammar was changed.""" + + self.devset = devset + """The development set -- a list of chunked sentences.""" + + self.devset_name = devset_name + """The name of the development set (for save files).""" + + self.devset_index = -1 + """The index into the development set of the first instance + that's currently being viewed.""" + + self._last_keypress = 0 + """The time() when a key was most recently pressed""" + + self._history = [] + """A list of (grammar, precision, recall, fscore) tuples for + grammars that the user has already tried.""" + + self._history_index = 0 + """When the user is scrolling through previous grammars, this + is used to keep track of which grammar they're looking at.""" + + self._eval_grammar = None + """The grammar that is being currently evaluated by the eval + demon.""" + + self._eval_normalized_grammar = None + """A normalized copy of ``_eval_grammar``.""" + + self._eval_index = 0 + """The index of the next sentence in the development set that + should be looked at by the eval demon.""" + + self._eval_score = ChunkScore(chunk_label=chunk_label) + """The ``ChunkScore`` object that's used to keep track of the score + of the current grammar on the development set.""" + + # Set up the main window. + top = self.top = Tk() + top.geometry("+50+50") + top.title("Regexp Chunk Parser App") + top.bind("", self.destroy) + + # Variable that restricts how much of the devset we look at. + self._devset_size = IntVar(top) + self._devset_size.set(100) + + # Set up all the tkinter widgets + self._init_fonts(top) + self._init_widgets(top) + self._init_bindings(top) + self._init_menubar(top) + self.grammarbox.focus() + + # If a grammar was given, then display it. + if grammar: + self.grammarbox.insert("end", grammar + "\n") + self.grammarbox.mark_set("insert", "1.0") + + # Display the first item in the development set + self.show_devset(0) + self.update() + + def _init_bindings(self, top): + top.bind("", self._devset_next) + top.bind("", self._devset_prev) + top.bind("", self.toggle_show_trace) + top.bind("", self.update) + top.bind("", lambda e: self.save_grammar()) + top.bind("", lambda e: self.load_grammar()) + self.grammarbox.bind("", self.toggle_show_trace) + self.grammarbox.bind("", self._devset_next) + self.grammarbox.bind("", self._devset_prev) + + # Redraw the eval graph when the window size changes + self.evalbox.bind("", self._eval_plot) + + def _init_fonts(self, top): + # TWhat's our font size (default=same as sysfont) + self._size = IntVar(top) + self._size.set(20) + self._font = Font(family="helvetica", size=-self._size.get()) + self._smallfont = Font( + family="helvetica", size=-(int(self._size.get() * 14 // 20)) + ) + + def _init_menubar(self, parent): + menubar = Menu(parent) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command(label="Reset Application", underline=0, command=self.reset) + filemenu.add_command( + label="Save Current Grammar", + underline=0, + accelerator="Ctrl-s", + command=self.save_grammar, + ) + filemenu.add_command( + label="Load Grammar", + underline=0, + accelerator="Ctrl-o", + command=self.load_grammar, + ) + + filemenu.add_command( + label="Save Grammar History", underline=13, command=self.save_history + ) + + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-q" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + viewmenu = Menu(menubar, tearoff=0) + viewmenu.add_radiobutton( + label="Tiny", + variable=self._size, + underline=0, + value=10, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Small", + variable=self._size, + underline=0, + value=16, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Medium", + variable=self._size, + underline=0, + value=20, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Large", + variable=self._size, + underline=0, + value=24, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Huge", + variable=self._size, + underline=0, + value=34, + command=self.resize, + ) + menubar.add_cascade(label="View", underline=0, menu=viewmenu) + + devsetmenu = Menu(menubar, tearoff=0) + devsetmenu.add_radiobutton( + label="50 sentences", + variable=self._devset_size, + value=50, + command=self.set_devset_size, + ) + devsetmenu.add_radiobutton( + label="100 sentences", + variable=self._devset_size, + value=100, + command=self.set_devset_size, + ) + devsetmenu.add_radiobutton( + label="200 sentences", + variable=self._devset_size, + value=200, + command=self.set_devset_size, + ) + devsetmenu.add_radiobutton( + label="500 sentences", + variable=self._devset_size, + value=500, + command=self.set_devset_size, + ) + menubar.add_cascade(label="Development-Set", underline=0, menu=devsetmenu) + + helpmenu = Menu(menubar, tearoff=0) + helpmenu.add_command(label="About", underline=0, command=self.about) + menubar.add_cascade(label="Help", underline=0, menu=helpmenu) + + parent.config(menu=menubar) + + def toggle_show_trace(self, *e): + if self._showing_trace: + self.show_devset() + else: + self.show_trace() + return "break" + + _SCALE_N = 5 # center on the last 5 examples. + _DRAW_LINES = False + + def _eval_plot(self, *e, **config): + width = config.get("width", self.evalbox.winfo_width()) + height = config.get("height", self.evalbox.winfo_height()) + + # Clear the canvas + self.evalbox.delete("all") + + # Draw the precision & recall labels. + tag = self.evalbox.create_text( + 10, height // 2 - 10, justify="left", anchor="w", text="Precision" + ) + left, right = self.evalbox.bbox(tag)[2] + 5, width - 10 + tag = self.evalbox.create_text( + left + (width - left) // 2, + height - 10, + anchor="s", + text="Recall", + justify="center", + ) + top, bot = 10, self.evalbox.bbox(tag)[1] - 10 + + # Draw masks for clipping the plot. + bg = self._EVALBOX_PARAMS["background"] + self.evalbox.lower( + self.evalbox.create_rectangle(0, 0, left - 1, 5000, fill=bg, outline=bg) + ) + self.evalbox.lower( + self.evalbox.create_rectangle(0, bot + 1, 5000, 5000, fill=bg, outline=bg) + ) + + # Calculate the plot's scale. + if self._autoscale.get() and len(self._history) > 1: + max_precision = max_recall = 0 + min_precision = min_recall = 1 + for i in range(1, min(len(self._history), self._SCALE_N + 1)): + grammar, precision, recall, fmeasure = self._history[-i] + min_precision = min(precision, min_precision) + min_recall = min(recall, min_recall) + max_precision = max(precision, max_precision) + max_recall = max(recall, max_recall) + # if max_precision-min_precision > max_recall-min_recall: + # min_recall -= (max_precision-min_precision)/2 + # max_recall += (max_precision-min_precision)/2 + # else: + # min_precision -= (max_recall-min_recall)/2 + # max_precision += (max_recall-min_recall)/2 + # if min_recall < 0: + # max_recall -= min_recall + # min_recall = 0 + # if min_precision < 0: + # max_precision -= min_precision + # min_precision = 0 + min_precision = max(min_precision - 0.01, 0) + min_recall = max(min_recall - 0.01, 0) + max_precision = min(max_precision + 0.01, 1) + max_recall = min(max_recall + 0.01, 1) + else: + min_precision = min_recall = 0 + max_precision = max_recall = 1 + + # Draw the axis lines & grid lines + for i in range(11): + x = left + (right - left) * ( + (i / 10.0 - min_recall) / (max_recall - min_recall) + ) + y = bot - (bot - top) * ( + (i / 10.0 - min_precision) / (max_precision - min_precision) + ) + if left < x < right: + self.evalbox.create_line(x, top, x, bot, fill="#888") + if top < y < bot: + self.evalbox.create_line(left, y, right, y, fill="#888") + self.evalbox.create_line(left, top, left, bot) + self.evalbox.create_line(left, bot, right, bot) + + # Display the plot's scale + self.evalbox.create_text( + left - 3, + bot, + justify="right", + anchor="se", + text="%d%%" % (100 * min_precision), + ) + self.evalbox.create_text( + left - 3, + top, + justify="right", + anchor="ne", + text="%d%%" % (100 * max_precision), + ) + self.evalbox.create_text( + left, + bot + 3, + justify="center", + anchor="nw", + text="%d%%" % (100 * min_recall), + ) + self.evalbox.create_text( + right, + bot + 3, + justify="center", + anchor="ne", + text="%d%%" % (100 * max_recall), + ) + + # Display the scores. + prev_x = prev_y = None + for i, (_, precision, recall, fscore) in enumerate(self._history): + x = left + (right - left) * ( + (recall - min_recall) / (max_recall - min_recall) + ) + y = bot - (bot - top) * ( + (precision - min_precision) / (max_precision - min_precision) + ) + if i == self._history_index: + self.evalbox.create_oval( + x - 2, y - 2, x + 2, y + 2, fill="#0f0", outline="#000" + ) + self.status["text"] = ( + "Precision: %.2f%%\t" % (precision * 100) + + "Recall: %.2f%%\t" % (recall * 100) + + "F-score: %.2f%%" % (fscore * 100) + ) + else: + self.evalbox.lower( + self.evalbox.create_oval( + x - 2, y - 2, x + 2, y + 2, fill="#afa", outline="#8c8" + ) + ) + if prev_x is not None and self._eval_lines.get(): + self.evalbox.lower( + self.evalbox.create_line(prev_x, prev_y, x, y, fill="#8c8") + ) + prev_x, prev_y = x, y + + _eval_demon_running = False + + def _eval_demon(self): + if self.top is None: + return + if self.chunker is None: + self._eval_demon_running = False + return + + # Note our starting time. + t0 = time.time() + + # If are still typing, then wait for them to finish. + if ( + time.time() - self._last_keypress < self._EVAL_DELAY + and self.normalized_grammar != self._eval_normalized_grammar + ): + self._eval_demon_running = True + return self.top.after(int(self._EVAL_FREQ * 1000), self._eval_demon) + + # If the grammar changed, restart the evaluation. + if self.normalized_grammar != self._eval_normalized_grammar: + # Check if we've seen this grammar already. If so, then + # just use the old evaluation values. + for g, p, r, f in self._history: + if self.normalized_grammar == self.normalize_grammar(g): + self._history.append((g, p, r, f)) + self._history_index = len(self._history) - 1 + self._eval_plot() + self._eval_demon_running = False + self._eval_normalized_grammar = None + return + self._eval_index = 0 + self._eval_score = ChunkScore(chunk_label=self._chunk_label) + self._eval_grammar = self.grammar + self._eval_normalized_grammar = self.normalized_grammar + + # If the grammar is empty, the don't bother evaluating it, or + # recording it in history -- the score will just be 0. + if self.normalized_grammar.strip() == "": + # self._eval_index = self._devset_size.get() + self._eval_demon_running = False + return + + # Score the next set of examples + for gold in self.devset[ + self._eval_index : min( + self._eval_index + self._EVAL_CHUNK, self._devset_size.get() + ) + ]: + guess = self._chunkparse(gold.leaves()) + self._eval_score.score(gold, guess) + + # update our index in the devset. + self._eval_index += self._EVAL_CHUNK + + # Check if we're done + if self._eval_index >= self._devset_size.get(): + self._history.append( + ( + self._eval_grammar, + self._eval_score.precision(), + self._eval_score.recall(), + self._eval_score.f_measure(), + ) + ) + self._history_index = len(self._history) - 1 + self._eval_plot() + self._eval_demon_running = False + self._eval_normalized_grammar = None + else: + progress = 100 * self._eval_index / self._devset_size.get() + self.status["text"] = "Evaluating on Development Set (%d%%)" % progress + self._eval_demon_running = True + self._adaptively_modify_eval_chunk(time.time() - t0) + self.top.after(int(self._EVAL_FREQ * 1000), self._eval_demon) + + def _adaptively_modify_eval_chunk(self, t): + """ + Modify _EVAL_CHUNK to try to keep the amount of time that the + eval demon takes between _EVAL_DEMON_MIN and _EVAL_DEMON_MAX. + + :param t: The amount of time that the eval demon took. + """ + if t > self._EVAL_DEMON_MAX and self._EVAL_CHUNK > 5: + self._EVAL_CHUNK = min( + self._EVAL_CHUNK - 1, + max( + int(self._EVAL_CHUNK * (self._EVAL_DEMON_MAX / t)), + self._EVAL_CHUNK - 10, + ), + ) + elif t < self._EVAL_DEMON_MIN: + self._EVAL_CHUNK = max( + self._EVAL_CHUNK + 1, + min( + int(self._EVAL_CHUNK * (self._EVAL_DEMON_MIN / t)), + self._EVAL_CHUNK + 10, + ), + ) + + def _init_widgets(self, top): + frame0 = Frame(top, **self._FRAME_PARAMS) + frame0.grid_columnconfigure(0, weight=4) + frame0.grid_columnconfigure(3, weight=2) + frame0.grid_rowconfigure(1, weight=1) + frame0.grid_rowconfigure(5, weight=1) + + # The grammar + self.grammarbox = Text(frame0, font=self._font, **self._GRAMMARBOX_PARAMS) + self.grammarlabel = Label( + frame0, + font=self._font, + text="Grammar:", + highlightcolor="black", + background=self._GRAMMARBOX_PARAMS["background"], + ) + self.grammarlabel.grid(column=0, row=0, sticky="SW") + self.grammarbox.grid(column=0, row=1, sticky="NEWS") + + # Scroll bar for grammar + grammar_scrollbar = Scrollbar(frame0, command=self.grammarbox.yview) + grammar_scrollbar.grid(column=1, row=1, sticky="NWS") + self.grammarbox.config(yscrollcommand=grammar_scrollbar.set) + + # grammar buttons + bg = self._FRAME_PARAMS["background"] + frame3 = Frame(frame0, background=bg) + frame3.grid(column=0, row=2, sticky="EW") + Button( + frame3, + text="Prev Grammar", + command=self._history_prev, + **self._BUTTON_PARAMS, + ).pack(side="left") + Button( + frame3, + text="Next Grammar", + command=self._history_next, + **self._BUTTON_PARAMS, + ).pack(side="left") + + # Help box + self.helpbox = Text(frame0, font=self._smallfont, **self._HELPBOX_PARAMS) + self.helpbox.grid(column=3, row=1, sticky="NEWS") + self.helptabs = {} + bg = self._FRAME_PARAMS["background"] + helptab_frame = Frame(frame0, background=bg) + helptab_frame.grid(column=3, row=0, sticky="SW") + for i, (tab, tabstops, text) in enumerate(self.HELP): + label = Label(helptab_frame, text=tab, font=self._smallfont) + label.grid(column=i * 2, row=0, sticky="S") + # help_frame.grid_columnconfigure(i, weight=1) + # label.pack(side='left') + label.bind("", lambda e, tab=tab: self.show_help(tab)) + self.helptabs[tab] = label + Frame( + helptab_frame, height=1, width=self._HELPTAB_SPACER, background=bg + ).grid(column=i * 2 + 1, row=0) + self.helptabs[self.HELP[0][0]].configure(font=self._font) + self.helpbox.tag_config("elide", elide=True) + for tag, params in self.HELP_AUTOTAG: + self.helpbox.tag_config("tag-%s" % tag, **params) + self.show_help(self.HELP[0][0]) + + # Scroll bar for helpbox + help_scrollbar = Scrollbar(frame0, command=self.helpbox.yview) + self.helpbox.config(yscrollcommand=help_scrollbar.set) + help_scrollbar.grid(column=4, row=1, sticky="NWS") + + # The dev set + frame4 = Frame(frame0, background=self._FRAME_PARAMS["background"]) + self.devsetbox = Text(frame4, font=self._font, **self._DEVSETBOX_PARAMS) + self.devsetbox.pack(expand=True, fill="both") + self.devsetlabel = Label( + frame0, + font=self._font, + text="Development Set:", + justify="right", + background=self._DEVSETBOX_PARAMS["background"], + ) + self.devsetlabel.grid(column=0, row=4, sticky="SW") + frame4.grid(column=0, row=5, sticky="NEWS") + + # dev set scrollbars + self.devset_scroll = Scrollbar(frame0, command=self._devset_scroll) + self.devset_scroll.grid(column=1, row=5, sticky="NWS") + self.devset_xscroll = Scrollbar( + frame4, command=self.devsetbox.xview, orient="horiz" + ) + self.devsetbox["xscrollcommand"] = self.devset_xscroll.set + self.devset_xscroll.pack(side="bottom", fill="x") + + # dev set buttons + bg = self._FRAME_PARAMS["background"] + frame1 = Frame(frame0, background=bg) + frame1.grid(column=0, row=7, sticky="EW") + Button( + frame1, + text="Prev Example (Ctrl-p)", + command=self._devset_prev, + **self._BUTTON_PARAMS, + ).pack(side="left") + Button( + frame1, + text="Next Example (Ctrl-n)", + command=self._devset_next, + **self._BUTTON_PARAMS, + ).pack(side="left") + self.devset_button = Button( + frame1, + text="Show example", + command=self.show_devset, + state="disabled", + **self._BUTTON_PARAMS, + ) + self.devset_button.pack(side="right") + self.trace_button = Button( + frame1, text="Show trace", command=self.show_trace, **self._BUTTON_PARAMS + ) + self.trace_button.pack(side="right") + + # evaluation box + self.evalbox = Canvas(frame0, **self._EVALBOX_PARAMS) + label = Label( + frame0, + font=self._font, + text="Evaluation:", + justify="right", + background=self._EVALBOX_PARAMS["background"], + ) + label.grid(column=3, row=4, sticky="SW") + self.evalbox.grid(column=3, row=5, sticky="NEWS", columnspan=2) + + # evaluation box buttons + bg = self._FRAME_PARAMS["background"] + frame2 = Frame(frame0, background=bg) + frame2.grid(column=3, row=7, sticky="EW") + self._autoscale = IntVar(self.top) + self._autoscale.set(False) + Checkbutton( + frame2, + variable=self._autoscale, + command=self._eval_plot, + text="Zoom", + **self._BUTTON_PARAMS, + ).pack(side="left") + self._eval_lines = IntVar(self.top) + self._eval_lines.set(False) + Checkbutton( + frame2, + variable=self._eval_lines, + command=self._eval_plot, + text="Lines", + **self._BUTTON_PARAMS, + ).pack(side="left") + Button(frame2, text="History", **self._BUTTON_PARAMS).pack(side="right") + + # The status label + self.status = Label(frame0, font=self._font, **self._STATUS_PARAMS) + self.status.grid(column=0, row=9, sticky="NEW", padx=3, pady=2, columnspan=5) + + # Help box & devset box can't be edited. + self.helpbox["state"] = "disabled" + self.devsetbox["state"] = "disabled" + + # Spacers + bg = self._FRAME_PARAMS["background"] + Frame(frame0, height=10, width=0, background=bg).grid(column=0, row=3) + Frame(frame0, height=0, width=10, background=bg).grid(column=2, row=0) + Frame(frame0, height=6, width=0, background=bg).grid(column=0, row=8) + + # pack the frame. + frame0.pack(fill="both", expand=True) + + # Set up colors for the devset box + self.devsetbox.tag_config("true-pos", background="#afa", underline="True") + self.devsetbox.tag_config("false-neg", underline="True", foreground="#800") + self.devsetbox.tag_config("false-pos", background="#faa") + self.devsetbox.tag_config("trace", foreground="#666", wrap="none") + self.devsetbox.tag_config("wrapindent", lmargin2=30, wrap="none") + self.devsetbox.tag_config("error", foreground="#800") + + # And for the grammarbox + self.grammarbox.tag_config("error", background="#fec") + self.grammarbox.tag_config("comment", foreground="#840") + self.grammarbox.tag_config("angle", foreground="#00f") + self.grammarbox.tag_config("brace", foreground="#0a0") + self.grammarbox.tag_config("hangindent", lmargin1=0, lmargin2=40) + + _showing_trace = False + + def show_trace(self, *e): + self._showing_trace = True + self.trace_button["state"] = "disabled" + self.devset_button["state"] = "normal" + + self.devsetbox["state"] = "normal" + # self.devsetbox['wrap'] = 'none' + self.devsetbox.delete("1.0", "end") + self.devsetlabel["text"] = "Development Set (%d/%d)" % ( + (self.devset_index + 1, self._devset_size.get()) + ) + + if self.chunker is None: + self.devsetbox.insert("1.0", "Trace: waiting for a valid grammar.") + self.devsetbox.tag_add("error", "1.0", "end") + return # can't do anything more + + gold_tree = self.devset[self.devset_index] + rules = self.chunker.rules() + + # Calculate the tag sequence + tagseq = "\t" + charnum = [1] + for wordnum, (word, pos) in enumerate(gold_tree.leaves()): + tagseq += "%s " % pos + charnum.append(len(tagseq)) + self.charnum = { + (i, j): charnum[j] + for i in range(len(rules) + 1) + for j in range(len(charnum)) + } + self.linenum = {i: i * 2 + 2 for i in range(len(rules) + 1)} + + for i in range(len(rules) + 1): + if i == 0: + self.devsetbox.insert("end", "Start:\n") + self.devsetbox.tag_add("trace", "end -2c linestart", "end -2c") + else: + self.devsetbox.insert("end", "Apply %s:\n" % rules[i - 1]) + self.devsetbox.tag_add("trace", "end -2c linestart", "end -2c") + # Display the tag sequence. + self.devsetbox.insert("end", tagseq + "\n") + self.devsetbox.tag_add("wrapindent", "end -2c linestart", "end -2c") + # Run a partial parser, and extract gold & test chunks + chunker = RegexpChunkParser(rules[:i]) + test_tree = self._chunkparse(gold_tree.leaves()) + gold_chunks = self._chunks(gold_tree) + test_chunks = self._chunks(test_tree) + # Compare them. + for chunk in gold_chunks.intersection(test_chunks): + self._color_chunk(i, chunk, "true-pos") + for chunk in gold_chunks - test_chunks: + self._color_chunk(i, chunk, "false-neg") + for chunk in test_chunks - gold_chunks: + self._color_chunk(i, chunk, "false-pos") + self.devsetbox.insert("end", "Finished.\n") + self.devsetbox.tag_add("trace", "end -2c linestart", "end -2c") + + # This is a hack, because the x-scrollbar isn't updating its + # position right -- I'm not sure what the underlying cause is + # though. (This is on OS X w/ python 2.5) + self.top.after(100, self.devset_xscroll.set, 0, 0.3) + + def show_help(self, tab): + self.helpbox["state"] = "normal" + self.helpbox.delete("1.0", "end") + for name, tabstops, text in self.HELP: + if name == tab: + text = text.replace( + "<>", + "\n".join( + "\t%s\t%s" % item + for item in sorted( + list(self.tagset.items()), + key=lambda t_w: re.match(r"\w+", t_w[0]) + and (0, t_w[0]) + or (1, t_w[0]), + ) + ), + ) + + self.helptabs[name].config(**self._HELPTAB_FG_PARAMS) + self.helpbox.config(tabs=tabstops) + self.helpbox.insert("1.0", text + "\n" * 20) + C = "1.0 + %d chars" + for tag, params in self.HELP_AUTOTAG: + pattern = f"(?s)(<{tag}>)(.*?)()" + for m in re.finditer(pattern, text): + self.helpbox.tag_add("elide", C % m.start(1), C % m.end(1)) + self.helpbox.tag_add( + "tag-%s" % tag, C % m.start(2), C % m.end(2) + ) + self.helpbox.tag_add("elide", C % m.start(3), C % m.end(3)) + else: + self.helptabs[name].config(**self._HELPTAB_BG_PARAMS) + self.helpbox["state"] = "disabled" + + def _history_prev(self, *e): + self._view_history(self._history_index - 1) + return "break" + + def _history_next(self, *e): + self._view_history(self._history_index + 1) + return "break" + + def _view_history(self, index): + # Bounds & sanity checking: + index = max(0, min(len(self._history) - 1, index)) + if not self._history: + return + # Already viewing the requested history item? + if index == self._history_index: + return + # Show the requested grammar. It will get added to _history + # only if they edit it (causing self.update() to get run.) + self.grammarbox["state"] = "normal" + self.grammarbox.delete("1.0", "end") + self.grammarbox.insert("end", self._history[index][0]) + self.grammarbox.mark_set("insert", "1.0") + self._history_index = index + self._syntax_highlight_grammar(self._history[index][0]) + # Record the normalized grammar & regenerate the chunker. + self.normalized_grammar = self.normalize_grammar(self._history[index][0]) + if self.normalized_grammar: + rules = [ + RegexpChunkRule.fromstring(line) + for line in self.normalized_grammar.split("\n") + ] + else: + rules = [] + self.chunker = RegexpChunkParser(rules) + # Show the score. + self._eval_plot() + # Update the devset box + self._highlight_devset() + if self._showing_trace: + self.show_trace() + # Update the grammar label + if self._history_index < len(self._history) - 1: + self.grammarlabel["text"] = "Grammar {}/{}:".format( + self._history_index + 1, + len(self._history), + ) + else: + self.grammarlabel["text"] = "Grammar:" + + def _devset_next(self, *e): + self._devset_scroll("scroll", 1, "page") + return "break" + + def _devset_prev(self, *e): + self._devset_scroll("scroll", -1, "page") + return "break" + + def destroy(self, *e): + if self.top is None: + return + self.top.destroy() + self.top = None + + def _devset_scroll(self, command, *args): + N = 1 # size of a page -- one sentence. + showing_trace = self._showing_trace + if command == "scroll" and args[1].startswith("unit"): + self.show_devset(self.devset_index + int(args[0])) + elif command == "scroll" and args[1].startswith("page"): + self.show_devset(self.devset_index + N * int(args[0])) + elif command == "moveto": + self.show_devset(int(float(args[0]) * self._devset_size.get())) + else: + assert 0, f"bad scroll command {command} {args}" + if showing_trace: + self.show_trace() + + def show_devset(self, index=None): + if index is None: + index = self.devset_index + + # Bounds checking + index = min(max(0, index), self._devset_size.get() - 1) + + if index == self.devset_index and not self._showing_trace: + return + self.devset_index = index + + self._showing_trace = False + self.trace_button["state"] = "normal" + self.devset_button["state"] = "disabled" + + # Clear the text box. + self.devsetbox["state"] = "normal" + self.devsetbox["wrap"] = "word" + self.devsetbox.delete("1.0", "end") + self.devsetlabel["text"] = "Development Set (%d/%d)" % ( + (self.devset_index + 1, self._devset_size.get()) + ) + + # Add the sentences + sample = self.devset[self.devset_index : self.devset_index + 1] + self.charnum = {} + self.linenum = {0: 1} + for sentnum, sent in enumerate(sample): + linestr = "" + for wordnum, (word, pos) in enumerate(sent.leaves()): + self.charnum[sentnum, wordnum] = len(linestr) + linestr += f"{word}/{pos} " + self.charnum[sentnum, wordnum + 1] = len(linestr) + self.devsetbox.insert("end", linestr[:-1] + "\n\n") + + # Highlight chunks in the dev set + if self.chunker is not None: + self._highlight_devset() + self.devsetbox["state"] = "disabled" + + # Update the scrollbar + first = self.devset_index / self._devset_size.get() + last = (self.devset_index + 2) / self._devset_size.get() + self.devset_scroll.set(first, last) + + def _chunks(self, tree): + chunks = set() + wordnum = 0 + for child in tree: + if isinstance(child, Tree): + if child.label() == self._chunk_label: + chunks.add((wordnum, wordnum + len(child))) + wordnum += len(child) + else: + wordnum += 1 + return chunks + + def _syntax_highlight_grammar(self, grammar): + if self.top is None: + return + self.grammarbox.tag_remove("comment", "1.0", "end") + self.grammarbox.tag_remove("angle", "1.0", "end") + self.grammarbox.tag_remove("brace", "1.0", "end") + self.grammarbox.tag_add("hangindent", "1.0", "end") + for lineno, line in enumerate(grammar.split("\n")): + if not line.strip(): + continue + m = re.match(r"(\\.|[^#])*(#.*)?", line) + comment_start = None + if m.group(2): + comment_start = m.start(2) + s = "%d.%d" % (lineno + 1, m.start(2)) + e = "%d.%d" % (lineno + 1, m.end(2)) + self.grammarbox.tag_add("comment", s, e) + for m in re.finditer("[<>{}]", line): + if comment_start is not None and m.start() >= comment_start: + break + s = "%d.%d" % (lineno + 1, m.start()) + e = "%d.%d" % (lineno + 1, m.end()) + if m.group() in "<>": + self.grammarbox.tag_add("angle", s, e) + else: + self.grammarbox.tag_add("brace", s, e) + + def _grammarcheck(self, grammar): + if self.top is None: + return + self.grammarbox.tag_remove("error", "1.0", "end") + self._grammarcheck_errs = [] + for lineno, line in enumerate(grammar.split("\n")): + line = re.sub(r"((\\.|[^#])*)(#.*)?", r"\1", line) + line = line.strip() + if line: + try: + RegexpChunkRule.fromstring(line) + except ValueError as e: + self.grammarbox.tag_add( + "error", "%s.0" % (lineno + 1), "%s.0 lineend" % (lineno + 1) + ) + self.status["text"] = "" + + def update(self, *event): + # Record when update was called (for grammarcheck) + if event: + self._last_keypress = time.time() + + # Read the grammar from the Text box. + self.grammar = grammar = self.grammarbox.get("1.0", "end") + + # If the grammar hasn't changed, do nothing: + normalized_grammar = self.normalize_grammar(grammar) + if normalized_grammar == self.normalized_grammar: + return + else: + self.normalized_grammar = normalized_grammar + + # If the grammar has changed, and we're looking at history, + # then stop looking at history. + if self._history_index < len(self._history) - 1: + self.grammarlabel["text"] = "Grammar:" + + self._syntax_highlight_grammar(grammar) + + # The grammar has changed; try parsing it. If it doesn't + # parse, do nothing. (flag error location?) + try: + # Note: the normalized grammar has no blank lines. + if normalized_grammar: + rules = [ + RegexpChunkRule.fromstring(line) + for line in normalized_grammar.split("\n") + ] + else: + rules = [] + except ValueError as e: + # Use the un-normalized grammar for error highlighting. + self._grammarcheck(grammar) + self.chunker = None + return + + self.chunker = RegexpChunkParser(rules) + self.grammarbox.tag_remove("error", "1.0", "end") + self.grammar_changed = time.time() + # Display the results + if self._showing_trace: + self.show_trace() + else: + self._highlight_devset() + # Start the eval demon + if not self._eval_demon_running: + self._eval_demon() + + def _highlight_devset(self, sample=None): + if sample is None: + sample = self.devset[self.devset_index : self.devset_index + 1] + + self.devsetbox.tag_remove("true-pos", "1.0", "end") + self.devsetbox.tag_remove("false-neg", "1.0", "end") + self.devsetbox.tag_remove("false-pos", "1.0", "end") + + # Run the grammar on the test cases. + for sentnum, gold_tree in enumerate(sample): + # Run the chunk parser + test_tree = self._chunkparse(gold_tree.leaves()) + # Extract gold & test chunks + gold_chunks = self._chunks(gold_tree) + test_chunks = self._chunks(test_tree) + # Compare them. + for chunk in gold_chunks.intersection(test_chunks): + self._color_chunk(sentnum, chunk, "true-pos") + for chunk in gold_chunks - test_chunks: + self._color_chunk(sentnum, chunk, "false-neg") + for chunk in test_chunks - gold_chunks: + self._color_chunk(sentnum, chunk, "false-pos") + + def _chunkparse(self, words): + try: + return self.chunker.parse(words) + except (ValueError, IndexError) as e: + # There's an error somewhere in the grammar, but we're not sure + # exactly where, so just mark the whole grammar as bad. + # E.g., this is caused by: "({})" + self.grammarbox.tag_add("error", "1.0", "end") + # Treat it as tagging nothing: + return words + + def _color_chunk(self, sentnum, chunk, tag): + start, end = chunk + self.devsetbox.tag_add( + tag, + f"{self.linenum[sentnum]}.{self.charnum[sentnum, start]}", + f"{self.linenum[sentnum]}.{self.charnum[sentnum, end] - 1}", + ) + + def reset(self): + # Clear various variables + self.chunker = None + self.grammar = None + self.normalized_grammar = None + self.grammar_changed = 0 + self._history = [] + self._history_index = 0 + # Update the on-screen display. + self.grammarbox.delete("1.0", "end") + self.show_devset(0) + self.update() + # self._eval_plot() + + SAVE_GRAMMAR_TEMPLATE = ( + "# Regexp Chunk Parsing Grammar\n" + "# Saved %(date)s\n" + "#\n" + "# Development set: %(devset)s\n" + "# Precision: %(precision)s\n" + "# Recall: %(recall)s\n" + "# F-score: %(fscore)s\n\n" + "%(grammar)s\n" + ) + + def save_grammar(self, filename=None): + if not filename: + ftypes = [("Chunk Gramamr", ".chunk"), ("All files", "*")] + filename = asksaveasfilename(filetypes=ftypes, defaultextension=".chunk") + if not filename: + return + if self._history and self.normalized_grammar == self.normalize_grammar( + self._history[-1][0] + ): + precision, recall, fscore = ( + "%.2f%%" % (100 * v) for v in self._history[-1][1:] + ) + elif self.chunker is None: + precision = recall = fscore = "Grammar not well formed" + else: + precision = recall = fscore = "Not finished evaluation yet" + + with open(filename, "w") as outfile: + outfile.write( + self.SAVE_GRAMMAR_TEMPLATE + % dict( + date=time.ctime(), + devset=self.devset_name, + precision=precision, + recall=recall, + fscore=fscore, + grammar=self.grammar.strip(), + ) + ) + + def load_grammar(self, filename=None): + if not filename: + ftypes = [("Chunk Gramamr", ".chunk"), ("All files", "*")] + filename = askopenfilename(filetypes=ftypes, defaultextension=".chunk") + if not filename: + return + self.grammarbox.delete("1.0", "end") + self.update() + with open(filename) as infile: + grammar = infile.read() + grammar = re.sub( + r"^\# Regexp Chunk Parsing Grammar[\s\S]*" "F-score:.*\n", "", grammar + ).lstrip() + self.grammarbox.insert("1.0", grammar) + self.update() + + def save_history(self, filename=None): + if not filename: + ftypes = [("Chunk Gramamr History", ".txt"), ("All files", "*")] + filename = asksaveasfilename(filetypes=ftypes, defaultextension=".txt") + if not filename: + return + + with open(filename, "w") as outfile: + outfile.write("# Regexp Chunk Parsing Grammar History\n") + outfile.write("# Saved %s\n" % time.ctime()) + outfile.write("# Development set: %s\n" % self.devset_name) + for i, (g, p, r, f) in enumerate(self._history): + hdr = ( + "Grammar %d/%d (precision=%.2f%%, recall=%.2f%%, " + "fscore=%.2f%%)" + % (i + 1, len(self._history), p * 100, r * 100, f * 100) + ) + outfile.write("\n%s\n" % hdr) + outfile.write("".join(" %s\n" % line for line in g.strip().split())) + + if not ( + self._history + and self.normalized_grammar + == self.normalize_grammar(self._history[-1][0]) + ): + if self.chunker is None: + outfile.write("\nCurrent Grammar (not well-formed)\n") + else: + outfile.write("\nCurrent Grammar (not evaluated)\n") + outfile.write( + "".join(" %s\n" % line for line in self.grammar.strip().split()) + ) + + def about(self, *e): + ABOUT = "NLTK RegExp Chunk Parser Application\n" + "Written by Edward Loper" + TITLE = "About: Regular Expression Chunk Parser Application" + try: + from tkinter.messagebox import Message + + Message(message=ABOUT, title=TITLE).show() + except Exception: + ShowText(self.top, TITLE, ABOUT) + + def set_devset_size(self, size=None): + if size is not None: + self._devset_size.set(size) + self._devset_size.set(min(len(self.devset), self._devset_size.get())) + self.show_devset(1) + self.show_devset(0) + # what about history? Evaluated at diff dev set sizes! + + def resize(self, size=None): + if size is not None: + self._size.set(size) + size = self._size.get() + self._font.configure(size=-(abs(size))) + self._smallfont.configure(size=min(-10, -(abs(size)) * 14 // 20)) + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this demo is created from a non-interactive program (e.g. + from a secript); otherwise, the demo will close as soon as + the script completes. + """ + if in_idle(): + return + self.top.mainloop(*args, **kwargs) + + +def app(): + RegexpChunkApp().mainloop() + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/collocations_app.py b/nltk/app/collocations_app.py new file mode 100644 index 0000000..ab97c90 --- /dev/null +++ b/nltk/app/collocations_app.py @@ -0,0 +1,438 @@ +# Natural Language Toolkit: Collocations Application +# Much of the GUI code is imported from concordance.py; We intend to merge these tools together +# Copyright (C) 2001-2026 NLTK Project +# Author: Sumukh Ghodke +# URL: +# For license information, see LICENSE.TXT +# + + +import queue as q +import threading +from tkinter import ( + END, + LEFT, + SUNKEN, + Button, + Frame, + IntVar, + Label, + Menu, + OptionMenu, + Scrollbar, + StringVar, + Text, + Tk, +) +from tkinter.font import Font + +from nltk.corpus import ( + alpino, + brown, + cess_cat, + cess_esp, + floresta, + indian, + mac_morpho, + machado, + nps_chat, + sinica_treebank, + treebank, +) +from nltk.probability import FreqDist +from nltk.util import in_idle + +CORPUS_LOADED_EVENT = "<>" +ERROR_LOADING_CORPUS_EVENT = "<>" +POLL_INTERVAL = 100 + +_DEFAULT = "English: Brown Corpus (Humor)" +_CORPORA = { + "Catalan: CESS-CAT Corpus": lambda: cess_cat.words(), + "English: Brown Corpus": lambda: brown.words(), + "English: Brown Corpus (Press)": lambda: brown.words( + categories=["news", "editorial", "reviews"] + ), + "English: Brown Corpus (Religion)": lambda: brown.words(categories="religion"), + "English: Brown Corpus (Learned)": lambda: brown.words(categories="learned"), + "English: Brown Corpus (Science Fiction)": lambda: brown.words( + categories="science_fiction" + ), + "English: Brown Corpus (Romance)": lambda: brown.words(categories="romance"), + "English: Brown Corpus (Humor)": lambda: brown.words(categories="humor"), + "English: NPS Chat Corpus": lambda: nps_chat.words(), + "English: Wall Street Journal Corpus": lambda: treebank.words(), + "Chinese: Sinica Corpus": lambda: sinica_treebank.words(), + "Dutch: Alpino Corpus": lambda: alpino.words(), + "Hindi: Indian Languages Corpus": lambda: indian.words(files="hindi.pos"), + "Portuguese: Floresta Corpus (Portugal)": lambda: floresta.words(), + "Portuguese: MAC-MORPHO Corpus (Brazil)": lambda: mac_morpho.words(), + "Portuguese: Machado Corpus (Brazil)": lambda: machado.words(), + "Spanish: CESS-ESP Corpus": lambda: cess_esp.words(), +} + + +class CollocationsView: + _BACKGROUND_COLOUR = "#FFF" # white + + def __init__(self): + self.queue = q.Queue() + self.model = CollocationsModel(self.queue) + self.top = Tk() + self._init_top(self.top) + self._init_menubar() + self._init_widgets(self.top) + self.load_corpus(self.model.DEFAULT_CORPUS) + self.after = self.top.after(POLL_INTERVAL, self._poll) + + def _init_top(self, top): + top.geometry("550x650+50+50") + top.title("NLTK Collocations List") + top.bind("", self.destroy) + top.protocol("WM_DELETE_WINDOW", self.destroy) + top.minsize(550, 650) + + def _init_widgets(self, parent): + self.main_frame = Frame( + parent, dict(background=self._BACKGROUND_COLOUR, padx=1, pady=1, border=1) + ) + self._init_corpus_select(self.main_frame) + self._init_results_box(self.main_frame) + self._init_paging(self.main_frame) + self._init_status(self.main_frame) + self.main_frame.pack(fill="both", expand=True) + + def _init_corpus_select(self, parent): + innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) + self.var = StringVar(innerframe) + self.var.set(self.model.DEFAULT_CORPUS) + Label( + innerframe, + justify=LEFT, + text=" Corpus: ", + background=self._BACKGROUND_COLOUR, + padx=2, + pady=1, + border=0, + ).pack(side="left") + + other_corpora = list(self.model.CORPORA.keys()).remove( + self.model.DEFAULT_CORPUS + ) + om = OptionMenu( + innerframe, + self.var, + self.model.DEFAULT_CORPUS, + command=self.corpus_selected, + *self.model.non_default_corpora() + ) + om["borderwidth"] = 0 + om["highlightthickness"] = 1 + om.pack(side="left") + innerframe.pack(side="top", fill="x", anchor="n") + + def _init_status(self, parent): + self.status = Label( + parent, + justify=LEFT, + relief=SUNKEN, + background=self._BACKGROUND_COLOUR, + border=0, + padx=1, + pady=0, + ) + self.status.pack(side="top", anchor="sw") + + def _init_menubar(self): + self._result_size = IntVar(self.top) + menubar = Menu(self.top) + + filemenu = Menu(menubar, tearoff=0, borderwidth=0) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-q" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + editmenu = Menu(menubar, tearoff=0) + rescntmenu = Menu(editmenu, tearoff=0) + rescntmenu.add_radiobutton( + label="20", + variable=self._result_size, + underline=0, + value=20, + command=self.set_result_size, + ) + rescntmenu.add_radiobutton( + label="50", + variable=self._result_size, + underline=0, + value=50, + command=self.set_result_size, + ) + rescntmenu.add_radiobutton( + label="100", + variable=self._result_size, + underline=0, + value=100, + command=self.set_result_size, + ) + rescntmenu.invoke(1) + editmenu.add_cascade(label="Result Count", underline=0, menu=rescntmenu) + + menubar.add_cascade(label="Edit", underline=0, menu=editmenu) + self.top.config(menu=menubar) + + def set_result_size(self, **kwargs): + self.model.result_count = self._result_size.get() + + def _init_results_box(self, parent): + innerframe = Frame(parent) + i1 = Frame(innerframe) + i2 = Frame(innerframe) + vscrollbar = Scrollbar(i1, borderwidth=1) + hscrollbar = Scrollbar(i2, borderwidth=1, orient="horiz") + self.results_box = Text( + i1, + font=Font(family="courier", size="16"), + state="disabled", + borderwidth=1, + yscrollcommand=vscrollbar.set, + xscrollcommand=hscrollbar.set, + wrap="none", + width="40", + height="20", + exportselection=1, + ) + self.results_box.pack(side="left", fill="both", expand=True) + vscrollbar.pack(side="left", fill="y", anchor="e") + vscrollbar.config(command=self.results_box.yview) + hscrollbar.pack(side="left", fill="x", expand=True, anchor="w") + hscrollbar.config(command=self.results_box.xview) + # there is no other way of avoiding the overlap of scrollbars while using pack layout manager!!! + Label(i2, text=" ", background=self._BACKGROUND_COLOUR).pack( + side="left", anchor="e" + ) + i1.pack(side="top", fill="both", expand=True, anchor="n") + i2.pack(side="bottom", fill="x", anchor="s") + innerframe.pack(side="top", fill="both", expand=True) + + def _init_paging(self, parent): + innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) + self.prev = prev = Button( + innerframe, + text="Previous", + command=self.previous, + width="10", + borderwidth=1, + highlightthickness=1, + state="disabled", + ) + prev.pack(side="left", anchor="center") + self.next = next = Button( + innerframe, + text="Next", + command=self.__next__, + width="10", + borderwidth=1, + highlightthickness=1, + state="disabled", + ) + next.pack(side="right", anchor="center") + innerframe.pack(side="top", fill="y") + self.reset_current_page() + + def reset_current_page(self): + self.current_page = -1 + + def _poll(self): + try: + event = self.queue.get(block=False) + except q.Empty: + pass + else: + if event == CORPUS_LOADED_EVENT: + self.handle_corpus_loaded(event) + elif event == ERROR_LOADING_CORPUS_EVENT: + self.handle_error_loading_corpus(event) + self.after = self.top.after(POLL_INTERVAL, self._poll) + + def handle_error_loading_corpus(self, event): + self.status["text"] = "Error in loading " + self.var.get() + self.unfreeze_editable() + self.clear_results_box() + self.freeze_editable() + self.reset_current_page() + + def handle_corpus_loaded(self, event): + self.status["text"] = self.var.get() + " is loaded" + self.unfreeze_editable() + self.clear_results_box() + self.reset_current_page() + # self.next() + collocations = self.model.next(self.current_page + 1) + self.write_results(collocations) + self.current_page += 1 + + def corpus_selected(self, *args): + new_selection = self.var.get() + self.load_corpus(new_selection) + + def previous(self): + self.freeze_editable() + collocations = self.model.prev(self.current_page - 1) + self.current_page = self.current_page - 1 + self.clear_results_box() + self.write_results(collocations) + self.unfreeze_editable() + + def __next__(self): + self.freeze_editable() + collocations = self.model.next(self.current_page + 1) + self.clear_results_box() + self.write_results(collocations) + self.current_page += 1 + self.unfreeze_editable() + + def load_corpus(self, selection): + if self.model.selected_corpus != selection: + self.status["text"] = "Loading " + selection + "..." + self.freeze_editable() + self.model.load_corpus(selection) + + def freeze_editable(self): + self.prev["state"] = "disabled" + self.next["state"] = "disabled" + + def clear_results_box(self): + self.results_box["state"] = "normal" + self.results_box.delete("1.0", END) + self.results_box["state"] = "disabled" + + def fire_event(self, event): + # Firing an event so that rendering of widgets happen in the mainloop thread + self.top.event_generate(event, when="tail") + + def destroy(self, *e): + if self.top is None: + return + self.top.after_cancel(self.after) + self.top.destroy() + self.top = None + + def mainloop(self, *args, **kwargs): + if in_idle(): + return + self.top.mainloop(*args, **kwargs) + + def unfreeze_editable(self): + self.set_paging_button_states() + + def set_paging_button_states(self): + if self.current_page == -1 or self.current_page == 0: + self.prev["state"] = "disabled" + else: + self.prev["state"] = "normal" + if self.model.is_last_page(self.current_page): + self.next["state"] = "disabled" + else: + self.next["state"] = "normal" + + def write_results(self, results): + self.results_box["state"] = "normal" + row = 1 + for each in results: + self.results_box.insert(str(row) + ".0", each[0] + " " + each[1] + "\n") + row += 1 + self.results_box["state"] = "disabled" + + +class CollocationsModel: + def __init__(self, queue): + self.result_count = None + self.selected_corpus = None + self.collocations = None + self.CORPORA = _CORPORA + self.DEFAULT_CORPUS = _DEFAULT + self.queue = queue + self.reset_results() + + def reset_results(self): + self.result_pages = [] + self.results_returned = 0 + + def load_corpus(self, name): + self.selected_corpus = name + self.collocations = None + runner_thread = self.LoadCorpus(name, self) + runner_thread.start() + self.reset_results() + + def non_default_corpora(self): + copy = [] + copy.extend(list(self.CORPORA.keys())) + copy.remove(self.DEFAULT_CORPUS) + copy.sort() + return copy + + def is_last_page(self, number): + if number < len(self.result_pages): + return False + return self.results_returned + ( + number - len(self.result_pages) + ) * self.result_count >= len(self.collocations) + + def next(self, page): + if (len(self.result_pages) - 1) < page: + for i in range(page - (len(self.result_pages) - 1)): + self.result_pages.append( + self.collocations[ + self.results_returned : self.results_returned + + self.result_count + ] + ) + self.results_returned += self.result_count + return self.result_pages[page] + + def prev(self, page): + if page == -1: + return [] + return self.result_pages[page] + + class LoadCorpus(threading.Thread): + def __init__(self, name, model): + threading.Thread.__init__(self) + self.model, self.name = model, name + + def run(self): + try: + words = self.model.CORPORA[self.name]() + from operator import itemgetter + + text = [w for w in words if len(w) > 2] + fd = FreqDist(tuple(text[i : i + 2]) for i in range(len(text) - 1)) + vocab = FreqDist(text) + scored = [ + ((w1, w2), fd[(w1, w2)] ** 3 / (vocab[w1] * vocab[w2])) + for w1, w2 in fd + ] + scored.sort(key=itemgetter(1), reverse=True) + self.model.collocations = list(map(itemgetter(0), scored)) + self.model.queue.put(CORPUS_LOADED_EVENT) + except Exception as e: + print(e) + self.model.queue.put(ERROR_LOADING_CORPUS_EVENT) + + +# def collocations(): +# colloc_strings = [w1 + ' ' + w2 for w1, w2 in self._collocations[:num]] + + +def app(): + c = CollocationsView() + c.mainloop() + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/concordance_app.py b/nltk/app/concordance_app.py new file mode 100755 index 0000000..7871664 --- /dev/null +++ b/nltk/app/concordance_app.py @@ -0,0 +1,709 @@ +# Natural Language Toolkit: Concordance Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Sumukh Ghodke +# URL: +# For license information, see LICENSE.TXT + +import queue as q +import re +import threading +from tkinter import ( + END, + LEFT, + SUNKEN, + Button, + Entry, + Frame, + IntVar, + Label, + Menu, + OptionMenu, + Scrollbar, + StringVar, + Text, + Tk, +) +from tkinter.font import Font + +from nltk.corpus import ( + alpino, + brown, + cess_cat, + cess_esp, + floresta, + indian, + mac_morpho, + nps_chat, + sinica_treebank, + treebank, +) +from nltk.draw.util import ShowText +from nltk.util import in_idle + +WORD_OR_TAG = "[^/ ]+" +BOUNDARY = r"\b" + +CORPUS_LOADED_EVENT = "<>" +SEARCH_TERMINATED_EVENT = "<>" +SEARCH_ERROR_EVENT = "<>" +ERROR_LOADING_CORPUS_EVENT = "<>" + +POLL_INTERVAL = 50 + +# NB All corpora must be specified in a lambda expression so as not to be +# loaded when the module is imported. + +_DEFAULT = "English: Brown Corpus (Humor, simplified)" +_CORPORA = { + "Catalan: CESS-CAT Corpus (simplified)": lambda: cess_cat.tagged_sents( + tagset="universal" + ), + "English: Brown Corpus": lambda: brown.tagged_sents(), + "English: Brown Corpus (simplified)": lambda: brown.tagged_sents( + tagset="universal" + ), + "English: Brown Corpus (Press, simplified)": lambda: brown.tagged_sents( + categories=["news", "editorial", "reviews"], tagset="universal" + ), + "English: Brown Corpus (Religion, simplified)": lambda: brown.tagged_sents( + categories="religion", tagset="universal" + ), + "English: Brown Corpus (Learned, simplified)": lambda: brown.tagged_sents( + categories="learned", tagset="universal" + ), + "English: Brown Corpus (Science Fiction, simplified)": lambda: brown.tagged_sents( + categories="science_fiction", tagset="universal" + ), + "English: Brown Corpus (Romance, simplified)": lambda: brown.tagged_sents( + categories="romance", tagset="universal" + ), + "English: Brown Corpus (Humor, simplified)": lambda: brown.tagged_sents( + categories="humor", tagset="universal" + ), + "English: NPS Chat Corpus": lambda: nps_chat.tagged_posts(), + "English: NPS Chat Corpus (simplified)": lambda: nps_chat.tagged_posts( + tagset="universal" + ), + "English: Wall Street Journal Corpus": lambda: treebank.tagged_sents(), + "English: Wall Street Journal Corpus (simplified)": lambda: treebank.tagged_sents( + tagset="universal" + ), + "Chinese: Sinica Corpus": lambda: sinica_treebank.tagged_sents(), + "Chinese: Sinica Corpus (simplified)": lambda: sinica_treebank.tagged_sents( + tagset="universal" + ), + "Dutch: Alpino Corpus": lambda: alpino.tagged_sents(), + "Dutch: Alpino Corpus (simplified)": lambda: alpino.tagged_sents( + tagset="universal" + ), + "Hindi: Indian Languages Corpus": lambda: indian.tagged_sents(files="hindi.pos"), + "Hindi: Indian Languages Corpus (simplified)": lambda: indian.tagged_sents( + files="hindi.pos", tagset="universal" + ), + "Portuguese: Floresta Corpus (Portugal)": lambda: floresta.tagged_sents(), + "Portuguese: Floresta Corpus (Portugal, simplified)": lambda: floresta.tagged_sents( + tagset="universal" + ), + "Portuguese: MAC-MORPHO Corpus (Brazil)": lambda: mac_morpho.tagged_sents(), + "Portuguese: MAC-MORPHO Corpus (Brazil, simplified)": lambda: mac_morpho.tagged_sents( + tagset="universal" + ), + "Spanish: CESS-ESP Corpus (simplified)": lambda: cess_esp.tagged_sents( + tagset="universal" + ), +} + + +class ConcordanceSearchView: + _BACKGROUND_COLOUR = "#FFF" # white + + # Colour of highlighted results + _HIGHLIGHT_WORD_COLOUR = "#F00" # red + _HIGHLIGHT_WORD_TAG = "HL_WRD_TAG" + + _HIGHLIGHT_LABEL_COLOUR = "#C0C0C0" # dark grey + _HIGHLIGHT_LABEL_TAG = "HL_LBL_TAG" + + # Percentage of text left of the scrollbar position + _FRACTION_LEFT_TEXT = 0.30 + + def __init__(self): + self.queue = q.Queue() + self.model = ConcordanceSearchModel(self.queue) + self.top = Tk() + self._init_top(self.top) + self._init_menubar() + self._init_widgets(self.top) + self.load_corpus(self.model.DEFAULT_CORPUS) + self.after = self.top.after(POLL_INTERVAL, self._poll) + + def _init_top(self, top): + top.geometry("950x680+50+50") + top.title("NLTK Concordance Search") + top.bind("", self.destroy) + top.protocol("WM_DELETE_WINDOW", self.destroy) + top.minsize(950, 680) + + def _init_widgets(self, parent): + self.main_frame = Frame( + parent, dict(background=self._BACKGROUND_COLOUR, padx=1, pady=1, border=1) + ) + self._init_corpus_select(self.main_frame) + self._init_query_box(self.main_frame) + self._init_results_box(self.main_frame) + self._init_paging(self.main_frame) + self._init_status(self.main_frame) + self.main_frame.pack(fill="both", expand=True) + + def _init_menubar(self): + self._result_size = IntVar(self.top) + self._cntx_bf_len = IntVar(self.top) + self._cntx_af_len = IntVar(self.top) + menubar = Menu(self.top) + + filemenu = Menu(menubar, tearoff=0, borderwidth=0) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-q" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + editmenu = Menu(menubar, tearoff=0) + rescntmenu = Menu(editmenu, tearoff=0) + rescntmenu.add_radiobutton( + label="20", + variable=self._result_size, + underline=0, + value=20, + command=self.set_result_size, + ) + rescntmenu.add_radiobutton( + label="50", + variable=self._result_size, + underline=0, + value=50, + command=self.set_result_size, + ) + rescntmenu.add_radiobutton( + label="100", + variable=self._result_size, + underline=0, + value=100, + command=self.set_result_size, + ) + rescntmenu.invoke(1) + editmenu.add_cascade(label="Result Count", underline=0, menu=rescntmenu) + + cntxmenu = Menu(editmenu, tearoff=0) + cntxbfmenu = Menu(cntxmenu, tearoff=0) + cntxbfmenu.add_radiobutton( + label="60 characters", + variable=self._cntx_bf_len, + underline=0, + value=60, + command=self.set_cntx_bf_len, + ) + cntxbfmenu.add_radiobutton( + label="80 characters", + variable=self._cntx_bf_len, + underline=0, + value=80, + command=self.set_cntx_bf_len, + ) + cntxbfmenu.add_radiobutton( + label="100 characters", + variable=self._cntx_bf_len, + underline=0, + value=100, + command=self.set_cntx_bf_len, + ) + cntxbfmenu.invoke(1) + cntxmenu.add_cascade(label="Before", underline=0, menu=cntxbfmenu) + + cntxafmenu = Menu(cntxmenu, tearoff=0) + cntxafmenu.add_radiobutton( + label="70 characters", + variable=self._cntx_af_len, + underline=0, + value=70, + command=self.set_cntx_af_len, + ) + cntxafmenu.add_radiobutton( + label="90 characters", + variable=self._cntx_af_len, + underline=0, + value=90, + command=self.set_cntx_af_len, + ) + cntxafmenu.add_radiobutton( + label="110 characters", + variable=self._cntx_af_len, + underline=0, + value=110, + command=self.set_cntx_af_len, + ) + cntxafmenu.invoke(1) + cntxmenu.add_cascade(label="After", underline=0, menu=cntxafmenu) + + editmenu.add_cascade(label="Context", underline=0, menu=cntxmenu) + + menubar.add_cascade(label="Edit", underline=0, menu=editmenu) + + self.top.config(menu=menubar) + + def set_result_size(self, **kwargs): + self.model.result_count = self._result_size.get() + + def set_cntx_af_len(self, **kwargs): + self._char_after = self._cntx_af_len.get() + + def set_cntx_bf_len(self, **kwargs): + self._char_before = self._cntx_bf_len.get() + + def _init_corpus_select(self, parent): + innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) + self.var = StringVar(innerframe) + self.var.set(self.model.DEFAULT_CORPUS) + Label( + innerframe, + justify=LEFT, + text=" Corpus: ", + background=self._BACKGROUND_COLOUR, + padx=2, + pady=1, + border=0, + ).pack(side="left") + + other_corpora = list(self.model.CORPORA.keys()).remove( + self.model.DEFAULT_CORPUS + ) + om = OptionMenu( + innerframe, + self.var, + self.model.DEFAULT_CORPUS, + command=self.corpus_selected, + *self.model.non_default_corpora() + ) + om["borderwidth"] = 0 + om["highlightthickness"] = 1 + om.pack(side="left") + innerframe.pack(side="top", fill="x", anchor="n") + + def _init_status(self, parent): + self.status = Label( + parent, + justify=LEFT, + relief=SUNKEN, + background=self._BACKGROUND_COLOUR, + border=0, + padx=1, + pady=0, + ) + self.status.pack(side="top", anchor="sw") + + def _init_query_box(self, parent): + innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) + another = Frame(innerframe, background=self._BACKGROUND_COLOUR) + self.query_box = Entry(another, width=60) + self.query_box.pack(side="left", fill="x", pady=25, anchor="center") + self.search_button = Button( + another, + text="Search", + command=self.search, + borderwidth=1, + highlightthickness=1, + ) + self.search_button.pack(side="left", fill="x", pady=25, anchor="center") + self.query_box.bind("", self.search_enter_keypress_handler) + another.pack() + innerframe.pack(side="top", fill="x", anchor="n") + + def search_enter_keypress_handler(self, *event): + self.search() + + def _init_results_box(self, parent): + innerframe = Frame(parent) + i1 = Frame(innerframe) + i2 = Frame(innerframe) + vscrollbar = Scrollbar(i1, borderwidth=1) + hscrollbar = Scrollbar(i2, borderwidth=1, orient="horiz") + self.results_box = Text( + i1, + font=Font(family="courier", size="16"), + state="disabled", + borderwidth=1, + yscrollcommand=vscrollbar.set, + xscrollcommand=hscrollbar.set, + wrap="none", + width="40", + height="20", + exportselection=1, + ) + self.results_box.pack(side="left", fill="both", expand=True) + self.results_box.tag_config( + self._HIGHLIGHT_WORD_TAG, foreground=self._HIGHLIGHT_WORD_COLOUR + ) + self.results_box.tag_config( + self._HIGHLIGHT_LABEL_TAG, foreground=self._HIGHLIGHT_LABEL_COLOUR + ) + vscrollbar.pack(side="left", fill="y", anchor="e") + vscrollbar.config(command=self.results_box.yview) + hscrollbar.pack(side="left", fill="x", expand=True, anchor="w") + hscrollbar.config(command=self.results_box.xview) + # there is no other way of avoiding the overlap of scrollbars while using pack layout manager!!! + Label(i2, text=" ", background=self._BACKGROUND_COLOUR).pack( + side="left", anchor="e" + ) + i1.pack(side="top", fill="both", expand=True, anchor="n") + i2.pack(side="bottom", fill="x", anchor="s") + innerframe.pack(side="top", fill="both", expand=True) + + def _init_paging(self, parent): + innerframe = Frame(parent, background=self._BACKGROUND_COLOUR) + self.prev = prev = Button( + innerframe, + text="Previous", + command=self.previous, + width="10", + borderwidth=1, + highlightthickness=1, + state="disabled", + ) + prev.pack(side="left", anchor="center") + self.next = next = Button( + innerframe, + text="Next", + command=self.__next__, + width="10", + borderwidth=1, + highlightthickness=1, + state="disabled", + ) + next.pack(side="right", anchor="center") + innerframe.pack(side="top", fill="y") + self.current_page = 0 + + def previous(self): + self.clear_results_box() + self.freeze_editable() + self.model.prev(self.current_page - 1) + + def __next__(self): + self.clear_results_box() + self.freeze_editable() + self.model.next(self.current_page + 1) + + def about(self, *e): + ABOUT = "NLTK Concordance Search Demo\n" + TITLE = "About: NLTK Concordance Search Demo" + try: + from tkinter.messagebox import Message + + Message(message=ABOUT, title=TITLE, parent=self.main_frame).show() + except Exception: + ShowText(self.top, TITLE, ABOUT) + + def _bind_event_handlers(self): + self.top.bind(CORPUS_LOADED_EVENT, self.handle_corpus_loaded) + self.top.bind(SEARCH_TERMINATED_EVENT, self.handle_search_terminated) + self.top.bind(SEARCH_ERROR_EVENT, self.handle_search_error) + self.top.bind(ERROR_LOADING_CORPUS_EVENT, self.handle_error_loading_corpus) + + def _poll(self): + try: + event = self.queue.get(block=False) + except q.Empty: + pass + else: + if event == CORPUS_LOADED_EVENT: + self.handle_corpus_loaded(event) + elif event == SEARCH_TERMINATED_EVENT: + self.handle_search_terminated(event) + elif event == SEARCH_ERROR_EVENT: + self.handle_search_error(event) + elif event == ERROR_LOADING_CORPUS_EVENT: + self.handle_error_loading_corpus(event) + self.after = self.top.after(POLL_INTERVAL, self._poll) + + def handle_error_loading_corpus(self, event): + self.status["text"] = "Error in loading " + self.var.get() + self.unfreeze_editable() + self.clear_all() + self.freeze_editable() + + def handle_corpus_loaded(self, event): + self.status["text"] = self.var.get() + " is loaded" + self.unfreeze_editable() + self.clear_all() + self.query_box.focus_set() + + def handle_search_terminated(self, event): + # todo: refactor the model such that it is less state sensitive + results = self.model.get_results() + self.write_results(results) + self.status["text"] = "" + if len(results) == 0: + self.status["text"] = "No results found for " + self.model.query + else: + self.current_page = self.model.last_requested_page + self.unfreeze_editable() + self.results_box.xview_moveto(self._FRACTION_LEFT_TEXT) + + def handle_search_error(self, event): + self.status["text"] = "Error in query " + self.model.query + self.unfreeze_editable() + + def corpus_selected(self, *args): + new_selection = self.var.get() + self.load_corpus(new_selection) + + def load_corpus(self, selection): + if self.model.selected_corpus != selection: + self.status["text"] = "Loading " + selection + "..." + self.freeze_editable() + self.model.load_corpus(selection) + + def search(self): + self.current_page = 0 + self.clear_results_box() + self.model.reset_results() + query = self.query_box.get() + if len(query.strip()) == 0: + return + self.status["text"] = "Searching for " + query + self.freeze_editable() + self.model.search(query, self.current_page + 1) + + def write_results(self, results): + self.results_box["state"] = "normal" + row = 1 + for each in results: + sent, pos1, pos2 = each[0].strip(), each[1], each[2] + if len(sent) != 0: + if pos1 < self._char_before: + sent, pos1, pos2 = self.pad(sent, pos1, pos2) + sentence = sent[pos1 - self._char_before : pos1 + self._char_after] + if not row == len(results): + sentence += "\n" + self.results_box.insert(str(row) + ".0", sentence) + word_markers, label_markers = self.words_and_labels(sent, pos1, pos2) + for marker in word_markers: + self.results_box.tag_add( + self._HIGHLIGHT_WORD_TAG, + str(row) + "." + str(marker[0]), + str(row) + "." + str(marker[1]), + ) + for marker in label_markers: + self.results_box.tag_add( + self._HIGHLIGHT_LABEL_TAG, + str(row) + "." + str(marker[0]), + str(row) + "." + str(marker[1]), + ) + row += 1 + self.results_box["state"] = "disabled" + + def words_and_labels(self, sentence, pos1, pos2): + search_exp = sentence[pos1:pos2] + words, labels = [], [] + labeled_words = search_exp.split(" ") + index = 0 + for each in labeled_words: + if each == "": + index += 1 + else: + word, label = each.split("/") + words.append( + (self._char_before + index, self._char_before + index + len(word)) + ) + index += len(word) + 1 + labels.append( + (self._char_before + index, self._char_before + index + len(label)) + ) + index += len(label) + index += 1 + return words, labels + + def pad(self, sent, hstart, hend): + if hstart >= self._char_before: + return sent, hstart, hend + d = self._char_before - hstart + sent = "".join([" "] * d) + sent + return sent, hstart + d, hend + d + + def destroy(self, *e): + if self.top is None: + return + self.top.after_cancel(self.after) + self.top.destroy() + self.top = None + + def clear_all(self): + self.query_box.delete(0, END) + self.model.reset_query() + self.clear_results_box() + + def clear_results_box(self): + self.results_box["state"] = "normal" + self.results_box.delete("1.0", END) + self.results_box["state"] = "disabled" + + def freeze_editable(self): + self.query_box["state"] = "disabled" + self.search_button["state"] = "disabled" + self.prev["state"] = "disabled" + self.next["state"] = "disabled" + + def unfreeze_editable(self): + self.query_box["state"] = "normal" + self.search_button["state"] = "normal" + self.set_paging_button_states() + + def set_paging_button_states(self): + if self.current_page == 0 or self.current_page == 1: + self.prev["state"] = "disabled" + else: + self.prev["state"] = "normal" + if self.model.has_more_pages(self.current_page): + self.next["state"] = "normal" + else: + self.next["state"] = "disabled" + + def fire_event(self, event): + # Firing an event so that rendering of widgets happen in the mainloop thread + self.top.event_generate(event, when="tail") + + def mainloop(self, *args, **kwargs): + if in_idle(): + return + self.top.mainloop(*args, **kwargs) + + +class ConcordanceSearchModel: + def __init__(self, queue): + self.queue = queue + self.CORPORA = _CORPORA + self.DEFAULT_CORPUS = _DEFAULT + self.selected_corpus = None + self.reset_query() + self.reset_results() + self.result_count = None + self.last_sent_searched = 0 + + def non_default_corpora(self): + copy = [] + copy.extend(list(self.CORPORA.keys())) + copy.remove(self.DEFAULT_CORPUS) + copy.sort() + return copy + + def load_corpus(self, name): + self.selected_corpus = name + self.tagged_sents = [] + runner_thread = self.LoadCorpus(name, self) + runner_thread.start() + + def search(self, query, page): + self.query = query + self.last_requested_page = page + self.SearchCorpus(self, page, self.result_count).start() + + def next(self, page): + self.last_requested_page = page + if len(self.results) < page: + self.search(self.query, page) + else: + self.queue.put(SEARCH_TERMINATED_EVENT) + + def prev(self, page): + self.last_requested_page = page + self.queue.put(SEARCH_TERMINATED_EVENT) + + def reset_results(self): + self.last_sent_searched = 0 + self.results = [] + self.last_page = None + + def reset_query(self): + self.query = None + + def set_results(self, page, resultset): + self.results.insert(page - 1, resultset) + + def get_results(self): + return self.results[self.last_requested_page - 1] + + def has_more_pages(self, page): + if self.results == [] or self.results[0] == []: + return False + if self.last_page is None: + return True + return page < self.last_page + + class LoadCorpus(threading.Thread): + def __init__(self, name, model): + threading.Thread.__init__(self) + self.model, self.name = model, name + + def run(self): + try: + ts = self.model.CORPORA[self.name]() + self.model.tagged_sents = [ + " ".join(w + "/" + t for (w, t) in sent) for sent in ts + ] + self.model.queue.put(CORPUS_LOADED_EVENT) + except Exception as e: + print(e) + self.model.queue.put(ERROR_LOADING_CORPUS_EVENT) + + class SearchCorpus(threading.Thread): + def __init__(self, model, page, count): + self.model, self.count, self.page = model, count, page + threading.Thread.__init__(self) + + def run(self): + q = self.processed_query() + sent_pos, i, sent_count = [], 0, 0 + for sent in self.model.tagged_sents[self.model.last_sent_searched :]: + try: + m = re.search(q, sent) + except re.error: + self.model.reset_results() + self.model.queue.put(SEARCH_ERROR_EVENT) + return + if m: + sent_pos.append((sent, m.start(), m.end())) + i += 1 + if i > self.count: + self.model.last_sent_searched += sent_count - 1 + break + sent_count += 1 + if self.count >= len(sent_pos): + self.model.last_sent_searched += sent_count - 1 + self.model.last_page = self.page + self.model.set_results(self.page, sent_pos) + else: + self.model.set_results(self.page, sent_pos[:-1]) + self.model.queue.put(SEARCH_TERMINATED_EVENT) + + def processed_query(self): + new = [] + for term in self.model.query.split(): + term = re.sub(r"\.", r"[^/ ]", term) + if re.match("[A-Z]+$", term): + new.append(BOUNDARY + WORD_OR_TAG + "/" + term + BOUNDARY) + elif "/" in term: + new.append(BOUNDARY + term + BOUNDARY) + else: + new.append(BOUNDARY + term + "/" + WORD_OR_TAG + BOUNDARY) + return " ".join(new) + + +def app(): + d = ConcordanceSearchView() + d.mainloop() + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/nemo_app.py b/nltk/app/nemo_app.py new file mode 100755 index 0000000..b2f015f --- /dev/null +++ b/nltk/app/nemo_app.py @@ -0,0 +1,163 @@ +# Finding (and Replacing) Nemo, Version 1.1, Aristide Grange 2006/06/06 +# https://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496783 + +""" +Finding (and Replacing) Nemo + +Instant Regular Expressions +Created by Aristide Grange +""" +import itertools +import re +from tkinter import SEL_FIRST, SEL_LAST, Frame, Label, PhotoImage, Scrollbar, Text, Tk + +windowTitle = "Finding (and Replacing) Nemo" +initialFind = r"n(.*?)e(.*?)m(.*?)o" +initialRepl = r"M\1A\2K\3I" +initialText = """\ +Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +""" +images = { + "FIND": "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", + "find": "R0lGODlhMAAiAPQAMf////f39+/v7+fn597e3tbW1s7OzsbGxr29vbW1ta2traWlpZycnJSUlIyMjISEhHt7e3Nzc2tra2NjY1paWlJSUkpKSkJCQjk5OSkpKRgYGAAAAAAAAAAAAAAAAAAAACH+AS4ALAAAAAAwACIAAAX/ICCOZGmeaKquY2AGLiuvMCAUBuHWc48Kh0iFInEYCb4kSQCxPBiMxkMigRQEgJiSFVBYHNGG0RiZOHjblWAiiY4fkDhEYoBp06dAWfyAQyKAgAwDaHgnB0RwgYASgQ0IhDuGJDAIFhMRVFSLEX8QCJJ4AQM5AgQHTZqqjBAOCQQEkWkCDRMUFQsICQ4Vm5maEwwHOAsPDTpKMAsUDlO4CssTcb+2DAp8YGCyNFoCEsZwFQ3QDRTTVBRS0g1QbgsCd5QAAwgIBwYFAwStzQ8UEdCKVchky0yVBw7YuXkAKt4IAg74vXHVagqFBRgXSCAyYWAVCH0SNhDTitCJfSL5/4RbAPKPhQYYjVCYYAvCP0BxEDaD8CheAAHNwqh8MMGPSwgLeJWhwHSjqkYI+xg4MMCEgQjtRvZ7UAYCpghMF7CxONOWJkYR+rCpY4JlVpVxKDwYWEactKW9mhYRtqCTgwgWEMArERSK1j5q//6T8KXonFsShpiJkAECgQYVjykooCVA0JGHEWNiYCHThTFeb3UkoiCCBgwGEKQ1kuAJlhFwhA71h5SukwUM5qqeCSGBgicEWkfNiWSERtBad4JNIBaQBaQah1ToyGZBAnsIuIJs1qnqiAIVjIE2gnAB1T5x0icgzXT79ipgMOOEH6HBbREBMJCeGEY08IoLAkzB1YYFwjxwSUGSNULQJnNUwRYlCcyEkALIxECAP9cNMMABYpRhy3ZsSLDaR70oUAiABGCkAxowCGCAAfDYIQACXoElGRsdXWDBdg2Y90IWktDYGYAB9PWHP0PMdFZaF07SQgAFNDAMAQg0QA1UC8xoZQl22JGFPgWkOUCOL1pZQyhjxinnnCWEAAA7", + "REPL": "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", + "repl": "R0lGODlhMAAjAPQAMf////f39+/v7+fn597e3tbW1s7OzsbGxr29vbW1ta2traWlpZycnJSUlIyMjISEhHt7e3Nzc2tra2NjY1paWlJSUkpKSkJCQjk5OTExMSkpKSEhIRgYGBAQEAgICAAAACH+AS4ALAAAAAAwACMAAAX/ICCOZGmeaKqubOu+gCDANBkIQ1EMQhAghFptYEAkEgjEwXBo7ISvweGgWCwUysPjwTgEoCafTySYIhYMxgLBjEQgCULvCw0QdAZdoVhUIJUFChISEAxYeQM1N1OMTAp+UwZ5eA4TEhFbDWYFdC4ECVMJjwl5BwsQa0umEhUVlhESDgqlBp0rAn5nVpBMDxeZDRQbHBgWFBSWDgtLBnFjKwRYCI9VqQsPs0YKEcMXFq0UEalFDWx4BAO2IwPjppAKDkrTWKYUGd7fEJJFEZpM00cOzCgh4EE8SaoWxKNixQooBRMyZMBwAYIRBhUgLDGS4MoBJeoANMhAgQsaCRZm/5lqaCUJhA4cNHjDoKEDBlJUHqkBlYBTiQUZNGjYMMxDhY3VWk6R4MEDBoMUak5AqoYBqANIBo4wcGGDUKIeLlzVZmWJggsVIkwAZaQSA3kdZzlKkIiEAAlDvW5oOkEBs488JTw44oeUIwdvVTFTUK7uiAAPgubt8GFDhQepqETAQCFU1UMGzlqAgFhUsAcCS0AO6lUDhw8xNRSbENGDhgWSHjWUe6ACbKITizmopZoBa6KvOwj9uuHDhwxyj3xekgDDhw5EvWKo0IB4iQLCOCC/njc7ZQ8UeGvza+ABZZgcxJNc4FO1gc0cOsCUrHevc8tdIMTIAhc4F198G2Qwwd8CBIQUAwEINABBBJUwR9R5wElgVRLwWODBBx4cGB8GEzDQIAo33CGJA8gh+JoH/clUgQU0YvDhdfmJdwEFC6Sjgg8yEPAABsPkh2F22cl2AQbn6QdTghTQ5eAJAQyQAAQV0MSBB9gRVZ4GE1mw5JZOAmiAVi1UWcAZDrDyZXYTeaOhA/bIVuIBPtKQ4h7ViYekUPdcEAEbzTzCRp5CADmAAwj+ORGPBcgwAAHo9ABGCYtm0ChwFHShlRiXhmHlkAcCiOeUodqQw5W0oXLAiamy4MOkjOyAaqxUymApDCEAADs=", +} +colors = ["#FF7B39", "#80F121"] +emphColors = ["#DAFC33", "#F42548"] +fieldParams = { + "height": 3, + "width": 70, + "font": ("monaco", 14), + "highlightthickness": 0, + "borderwidth": 0, + "background": "white", +} +textParams = { + "bg": "#F7E0D4", + "fg": "#2321F1", + "highlightthickness": 0, + "width": 1, + "height": 10, + "font": ("verdana", 16), + "wrap": "word", +} + + +class Zone: + def __init__(self, image, initialField, initialText): + frm = Frame(root) + frm.config(background="white") + self.image = PhotoImage(format="gif", data=images[image.upper()]) + self.imageDimmed = PhotoImage(format="gif", data=images[image]) + self.img = Label(frm) + self.img.config(borderwidth=0) + self.img.pack(side="left") + self.fld = Text(frm, **fieldParams) + self.initScrollText(frm, self.fld, initialField) + frm = Frame(root) + self.txt = Text(frm, **textParams) + self.initScrollText(frm, self.txt, initialText) + for i in range(2): + self.txt.tag_config(colors[i], background=colors[i]) + self.txt.tag_config("emph" + colors[i], foreground=emphColors[i]) + + def initScrollText(self, frm, txt, contents): + scl = Scrollbar(frm) + scl.config(command=txt.yview) + scl.pack(side="right", fill="y") + txt.pack(side="left", expand=True, fill="x") + txt.config(yscrollcommand=scl.set) + txt.insert("1.0", contents) + frm.pack(fill="x") + Frame(height=2, bd=1, relief="ridge").pack(fill="x") + + def refresh(self): + self.colorCycle = itertools.cycle(colors) + try: + self.substitute() + self.img.config(image=self.image) + except re.error: + self.img.config(image=self.imageDimmed) + + +class FindZone(Zone): + def addTags(self, m): + color = next(self.colorCycle) + self.txt.tag_add(color, "1.0+%sc" % m.start(), "1.0+%sc" % m.end()) + try: + self.txt.tag_add( + "emph" + color, "1.0+%sc" % m.start("emph"), "1.0+%sc" % m.end("emph") + ) + except Exception: + pass + + def substitute(self, *args): + for color in colors: + self.txt.tag_remove(color, "1.0", "end") + self.txt.tag_remove("emph" + color, "1.0", "end") + self.rex = re.compile("") # default value in case of malformed regexp + self.rex = re.compile(self.fld.get("1.0", "end")[:-1], re.MULTILINE) + try: + re.compile("(?P%s)" % self.fld.get(SEL_FIRST, SEL_LAST)) + self.rexSel = re.compile( + "%s(?P%s)%s" + % ( + self.fld.get("1.0", SEL_FIRST), + self.fld.get(SEL_FIRST, SEL_LAST), + self.fld.get(SEL_LAST, "end")[:-1], + ), + re.MULTILINE, + ) + except Exception: + self.rexSel = self.rex + self.rexSel.sub(self.addTags, self.txt.get("1.0", "end")) + + +class ReplaceZone(Zone): + def addTags(self, m): + s = sz.rex.sub(self.repl, m.group()) + self.txt.delete( + "1.0+%sc" % (m.start() + self.diff), "1.0+%sc" % (m.end() + self.diff) + ) + self.txt.insert("1.0+%sc" % (m.start() + self.diff), s, next(self.colorCycle)) + self.diff += len(s) - (m.end() - m.start()) + + def substitute(self): + self.txt.delete("1.0", "end") + self.txt.insert("1.0", sz.txt.get("1.0", "end")[:-1]) + self.diff = 0 + self.repl = rex0.sub(r"\\g<\1>", self.fld.get("1.0", "end")[:-1]) + sz.rex.sub(self.addTags, sz.txt.get("1.0", "end")[:-1]) + + +def launchRefresh(_): + sz.fld.after_idle(sz.refresh) + rz.fld.after_idle(rz.refresh) + + +def app(): + global root, sz, rz, rex0 + root = Tk() + root.resizable(height=False, width=True) + root.title(windowTitle) + root.minsize(width=250, height=0) + sz = FindZone("find", initialFind, initialText) + sz.fld.bind("", launchRefresh) + sz.fld.bind("", launchRefresh) + sz.fld.bind("", launchRefresh) + sz.rexSel = re.compile("") + rz = ReplaceZone("repl", initialRepl, "") + rex0 = re.compile(r"(?", launchRefresh) + launchRefresh(None) + root.mainloop() + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/rdparser_app.py b/nltk/app/rdparser_app.py new file mode 100644 index 0000000..09e755c --- /dev/null +++ b/nltk/app/rdparser_app.py @@ -0,0 +1,1052 @@ +# Natural Language Toolkit: Recursive Descent Parser Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A graphical tool for exploring the recursive descent parser. + +The recursive descent parser maintains a tree, which records the +structure of the portion of the text that has been parsed. It uses +CFG productions to expand the fringe of the tree, and matches its +leaves against the text. Initially, the tree contains the start +symbol ("S"). It is shown in the main canvas, to the right of the +list of available expansions. + +The parser builds up a tree structure for the text using three +operations: + + - "expand" uses a CFG production to add children to a node on the + fringe of the tree. + - "match" compares a leaf in the tree to a text token. + - "backtrack" returns the tree to its state before the most recent + expand or match operation. + +The parser maintains a list of tree locations called a "frontier" to +remember which nodes have not yet been expanded and which leaves have +not yet been matched against the text. The leftmost frontier node is +shown in green, and the other frontier nodes are shown in blue. The +parser always performs expand and match operations on the leftmost +element of the frontier. + +You can control the parser's operation by using the "expand," "match," +and "backtrack" buttons; or you can use the "step" button to let the +parser automatically decide which operation to apply. The parser uses +the following rules to decide which operation to apply: + + - If the leftmost frontier element is a token, try matching it. + - If the leftmost frontier element is a node, try expanding it with + the first untried expansion. + - Otherwise, backtrack. + +The "expand" button applies the untried expansion whose CFG production +is listed earliest in the grammar. To manually choose which expansion +to apply, click on a CFG production from the list of available +expansions, on the left side of the main window. + +The "autostep" button will let the parser continue applying +applications to the tree until it reaches a complete parse. You can +cancel an autostep in progress at any time by clicking on the +"autostep" button again. + +Keyboard Shortcuts:: + [Space]\t Perform the next expand, match, or backtrack operation + [a]\t Step through operations until the next complete parse + [e]\t Perform an expand operation + [m]\t Perform a match operation + [b]\t Perform a backtrack operation + [Delete]\t Reset the parser + [g]\t Show/hide available expansions list + [h]\t Help + [Ctrl-p]\t Print + [q]\t Quit +""" + +from tkinter import Button, Frame, IntVar, Label, Listbox, Menu, Scrollbar, Tk +from tkinter.font import Font + +from nltk.draw import CFGEditor, TreeSegmentWidget, tree_to_treesegment +from nltk.draw.util import CanvasFrame, EntryDialog, ShowText, TextWidget +from nltk.parse import SteppingRecursiveDescentParser +from nltk.tree import Tree +from nltk.util import in_idle + + +class RecursiveDescentApp: + """ + A graphical tool for exploring the recursive descent parser. The tool + displays the parser's tree and the remaining text, and allows the + user to control the parser's operation. In particular, the user + can expand subtrees on the frontier, match tokens on the frontier + against the text, and backtrack. A "step" button simply steps + through the parsing process, performing the operations that + ``RecursiveDescentParser`` would use. + """ + + def __init__(self, grammar, sent, trace=0): + self._sent = sent + self._parser = SteppingRecursiveDescentParser(grammar, trace) + + # Set up the main window. + self._top = Tk() + self._top.title("Recursive Descent Parser Application") + + # Set up key bindings. + self._init_bindings() + + # Initialize the fonts. + self._init_fonts(self._top) + + # Animations. animating_lock is a lock to prevent the demo + # from performing new operations while it's animating. + self._animation_frames = IntVar(self._top) + self._animation_frames.set(5) + self._animating_lock = 0 + self._autostep = 0 + + # The user can hide the grammar. + self._show_grammar = IntVar(self._top) + self._show_grammar.set(1) + + # Create the basic frames. + self._init_menubar(self._top) + self._init_buttons(self._top) + self._init_feedback(self._top) + self._init_grammar(self._top) + self._init_canvas(self._top) + + # Initialize the parser. + self._parser.initialize(self._sent) + + # Resize callback + self._canvas.bind("", self._configure) + + ######################################### + ## Initialization Helpers + ######################################### + + def _init_fonts(self, root): + # See: + self._sysfont = Font(font=Button()["font"]) + root.option_add("*Font", self._sysfont) + + # TWhat's our font size (default=same as sysfont) + self._size = IntVar(root) + self._size.set(self._sysfont.cget("size")) + + self._boldfont = Font(family="helvetica", weight="bold", size=self._size.get()) + self._font = Font(family="helvetica", size=self._size.get()) + if self._size.get() < 0: + big = self._size.get() - 2 + else: + big = self._size.get() + 2 + self._bigfont = Font(family="helvetica", weight="bold", size=big) + + def _init_grammar(self, parent): + # Grammar view. + self._prodframe = listframe = Frame(parent) + self._prodframe.pack(fill="both", side="left", padx=2) + self._prodlist_label = Label( + self._prodframe, font=self._boldfont, text="Available Expansions" + ) + self._prodlist_label.pack() + self._prodlist = Listbox( + self._prodframe, + selectmode="single", + relief="groove", + background="white", + foreground="#909090", + font=self._font, + selectforeground="#004040", + selectbackground="#c0f0c0", + ) + + self._prodlist.pack(side="right", fill="both", expand=1) + + self._productions = list(self._parser.grammar().productions()) + for production in self._productions: + self._prodlist.insert("end", (" %s" % production)) + self._prodlist.config(height=min(len(self._productions), 25)) + + # Add a scrollbar if there are more than 25 productions. + if len(self._productions) > 25: + listscroll = Scrollbar(self._prodframe, orient="vertical") + self._prodlist.config(yscrollcommand=listscroll.set) + listscroll.config(command=self._prodlist.yview) + listscroll.pack(side="left", fill="y") + + # If they select a production, apply it. + self._prodlist.bind("<>", self._prodlist_select) + + def _init_bindings(self): + # Key bindings are a good thing. + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + self._top.bind("e", self.expand) + # self._top.bind('', self.expand) + # self._top.bind('', self.expand) + self._top.bind("m", self.match) + self._top.bind("", self.match) + self._top.bind("", self.match) + self._top.bind("b", self.backtrack) + self._top.bind("", self.backtrack) + self._top.bind("", self.backtrack) + self._top.bind("", self.backtrack) + self._top.bind("", self.backtrack) + self._top.bind("a", self.autostep) + # self._top.bind('', self.autostep) + self._top.bind("", self.autostep) + self._top.bind("", self.cancel_autostep) + self._top.bind("", self.step) + self._top.bind("", self.reset) + self._top.bind("", self.postscript) + # self._top.bind('', self.help) + # self._top.bind('', self.help) + self._top.bind("", self.help) + self._top.bind("", self.help) + # self._top.bind('', self.toggle_grammar) + # self._top.bind('', self.toggle_grammar) + # self._top.bind('', self.toggle_grammar) + self._top.bind("", self.edit_grammar) + self._top.bind("", self.edit_sentence) + + def _init_buttons(self, parent): + # Set up the frames. + self._buttonframe = buttonframe = Frame(parent) + buttonframe.pack(fill="none", side="bottom", padx=3, pady=2) + Button( + buttonframe, + text="Step", + background="#90c0d0", + foreground="black", + command=self.step, + ).pack(side="left") + Button( + buttonframe, + text="Autostep", + background="#90c0d0", + foreground="black", + command=self.autostep, + ).pack(side="left") + Button( + buttonframe, + text="Expand", + underline=0, + background="#90f090", + foreground="black", + command=self.expand, + ).pack(side="left") + Button( + buttonframe, + text="Match", + underline=0, + background="#90f090", + foreground="black", + command=self.match, + ).pack(side="left") + Button( + buttonframe, + text="Backtrack", + underline=0, + background="#f0a0a0", + foreground="black", + command=self.backtrack, + ).pack(side="left") + # Replace autostep... + + # self._autostep_button = Button(buttonframe, text='Autostep', + # underline=0, command=self.autostep) + # self._autostep_button.pack(side='left') + + def _configure(self, event): + self._autostep = 0 + (x1, y1, x2, y2) = self._cframe.scrollregion() + y2 = event.height - 6 + self._canvas["scrollregion"] = "%d %d %d %d" % (x1, y1, x2, y2) + self._redraw() + + def _init_feedback(self, parent): + self._feedbackframe = feedbackframe = Frame(parent) + feedbackframe.pack(fill="x", side="bottom", padx=3, pady=3) + self._lastoper_label = Label( + feedbackframe, text="Last Operation:", font=self._font + ) + self._lastoper_label.pack(side="left") + lastoperframe = Frame(feedbackframe, relief="sunken", border=1) + lastoperframe.pack(fill="x", side="right", expand=1, padx=5) + self._lastoper1 = Label( + lastoperframe, foreground="#007070", background="#f0f0f0", font=self._font + ) + self._lastoper2 = Label( + lastoperframe, + anchor="w", + width=30, + foreground="#004040", + background="#f0f0f0", + font=self._font, + ) + self._lastoper1.pack(side="left") + self._lastoper2.pack(side="left", fill="x", expand=1) + + def _init_canvas(self, parent): + self._cframe = CanvasFrame( + parent, + background="white", + # width=525, height=250, + closeenough=10, + border=2, + relief="sunken", + ) + self._cframe.pack(expand=1, fill="both", side="top", pady=2) + canvas = self._canvas = self._cframe.canvas() + + # Initially, there's no tree or text + self._tree = None + self._textwidgets = [] + self._textline = None + + def _init_menubar(self, parent): + menubar = Menu(parent) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Reset Parser", underline=0, command=self.reset, accelerator="Del" + ) + filemenu.add_command( + label="Print to Postscript", + underline=0, + command=self.postscript, + accelerator="Ctrl-p", + ) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + editmenu = Menu(menubar, tearoff=0) + editmenu.add_command( + label="Edit Grammar", + underline=5, + command=self.edit_grammar, + accelerator="Ctrl-g", + ) + editmenu.add_command( + label="Edit Text", + underline=5, + command=self.edit_sentence, + accelerator="Ctrl-t", + ) + menubar.add_cascade(label="Edit", underline=0, menu=editmenu) + + rulemenu = Menu(menubar, tearoff=0) + rulemenu.add_command( + label="Step", underline=1, command=self.step, accelerator="Space" + ) + rulemenu.add_separator() + rulemenu.add_command( + label="Match", underline=0, command=self.match, accelerator="Ctrl-m" + ) + rulemenu.add_command( + label="Expand", underline=0, command=self.expand, accelerator="Ctrl-e" + ) + rulemenu.add_separator() + rulemenu.add_command( + label="Backtrack", underline=0, command=self.backtrack, accelerator="Ctrl-b" + ) + menubar.add_cascade(label="Apply", underline=0, menu=rulemenu) + + viewmenu = Menu(menubar, tearoff=0) + viewmenu.add_checkbutton( + label="Show Grammar", + underline=0, + variable=self._show_grammar, + command=self._toggle_grammar, + ) + viewmenu.add_separator() + viewmenu.add_radiobutton( + label="Tiny", + variable=self._size, + underline=0, + value=10, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Small", + variable=self._size, + underline=0, + value=12, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Medium", + variable=self._size, + underline=0, + value=14, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Large", + variable=self._size, + underline=0, + value=18, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Huge", + variable=self._size, + underline=0, + value=24, + command=self.resize, + ) + menubar.add_cascade(label="View", underline=0, menu=viewmenu) + + animatemenu = Menu(menubar, tearoff=0) + animatemenu.add_radiobutton( + label="No Animation", underline=0, variable=self._animation_frames, value=0 + ) + animatemenu.add_radiobutton( + label="Slow Animation", + underline=0, + variable=self._animation_frames, + value=10, + accelerator="-", + ) + animatemenu.add_radiobutton( + label="Normal Animation", + underline=0, + variable=self._animation_frames, + value=5, + accelerator="=", + ) + animatemenu.add_radiobutton( + label="Fast Animation", + underline=0, + variable=self._animation_frames, + value=2, + accelerator="+", + ) + menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) + + helpmenu = Menu(menubar, tearoff=0) + helpmenu.add_command(label="About", underline=0, command=self.about) + helpmenu.add_command( + label="Instructions", underline=0, command=self.help, accelerator="F1" + ) + menubar.add_cascade(label="Help", underline=0, menu=helpmenu) + + parent.config(menu=menubar) + + ######################################### + ## Helper + ######################################### + + def _get(self, widget, treeloc): + for i in treeloc: + widget = widget.subtrees()[i] + if isinstance(widget, TreeSegmentWidget): + widget = widget.label() + return widget + + ######################################### + ## Main draw procedure + ######################################### + + def _redraw(self): + canvas = self._canvas + + # Delete the old tree, widgets, etc. + if self._tree is not None: + self._cframe.destroy_widget(self._tree) + for twidget in self._textwidgets: + self._cframe.destroy_widget(twidget) + if self._textline is not None: + self._canvas.delete(self._textline) + + # Draw the tree. + helv = ("helvetica", -self._size.get()) + bold = ("helvetica", -self._size.get(), "bold") + attribs = { + "tree_color": "#000000", + "tree_width": 2, + "node_font": bold, + "leaf_font": helv, + } + tree = self._parser.tree() + self._tree = tree_to_treesegment(canvas, tree, **attribs) + self._cframe.add_widget(self._tree, 30, 5) + + # Draw the text. + helv = ("helvetica", -self._size.get()) + bottom = y = self._cframe.scrollregion()[3] + self._textwidgets = [ + TextWidget(canvas, word, font=self._font) for word in self._sent + ] + for twidget in self._textwidgets: + self._cframe.add_widget(twidget, 0, 0) + twidget.move(0, bottom - twidget.bbox()[3] - 5) + y = min(y, twidget.bbox()[1]) + + # Draw a line over the text, to separate it from the tree. + self._textline = canvas.create_line(-5000, y - 5, 5000, y - 5, dash=".") + + # Highlight appropriate nodes. + self._highlight_nodes() + self._highlight_prodlist() + + # Make sure the text lines up. + self._position_text() + + def _redraw_quick(self): + # This should be more-or-less sufficient after an animation. + self._highlight_nodes() + self._highlight_prodlist() + self._position_text() + + def _highlight_nodes(self): + # Highlight the list of nodes to be checked. + bold = ("helvetica", -self._size.get(), "bold") + for treeloc in self._parser.frontier()[:1]: + self._get(self._tree, treeloc)["color"] = "#20a050" + self._get(self._tree, treeloc)["font"] = bold + for treeloc in self._parser.frontier()[1:]: + self._get(self._tree, treeloc)["color"] = "#008080" + + def _highlight_prodlist(self): + # Highlight the productions that can be expanded. + # Boy, too bad tkinter doesn't implement Listbox.itemconfig; + # that would be pretty useful here. + self._prodlist.delete(0, "end") + expandable = self._parser.expandable_productions() + untried = self._parser.untried_expandable_productions() + productions = self._productions + for index in range(len(productions)): + if productions[index] in expandable: + if productions[index] in untried: + self._prodlist.insert(index, " %s" % productions[index]) + else: + self._prodlist.insert(index, " %s (TRIED)" % productions[index]) + self._prodlist.selection_set(index) + else: + self._prodlist.insert(index, " %s" % productions[index]) + + def _position_text(self): + # Line up the text widgets that are matched against the tree + numwords = len(self._sent) + num_matched = numwords - len(self._parser.remaining_text()) + leaves = self._tree_leaves()[:num_matched] + xmax = self._tree.bbox()[0] + for i in range(0, len(leaves)): + widget = self._textwidgets[i] + leaf = leaves[i] + widget["color"] = "#006040" + leaf["color"] = "#006040" + widget.move(leaf.bbox()[0] - widget.bbox()[0], 0) + xmax = widget.bbox()[2] + 10 + + # Line up the text widgets that are not matched against the tree. + for i in range(len(leaves), numwords): + widget = self._textwidgets[i] + widget["color"] = "#a0a0a0" + widget.move(xmax - widget.bbox()[0], 0) + xmax = widget.bbox()[2] + 10 + + # If we have a complete parse, make everything green :) + if self._parser.currently_complete(): + for twidget in self._textwidgets: + twidget["color"] = "#00a000" + + # Move the matched leaves down to the text. + for i in range(0, len(leaves)): + widget = self._textwidgets[i] + leaf = leaves[i] + dy = widget.bbox()[1] - leaf.bbox()[3] - 10.0 + dy = max(dy, leaf.parent().label().bbox()[3] - leaf.bbox()[3] + 10) + leaf.move(0, dy) + + def _tree_leaves(self, tree=None): + if tree is None: + tree = self._tree + if isinstance(tree, TreeSegmentWidget): + leaves = [] + for child in tree.subtrees(): + leaves += self._tree_leaves(child) + return leaves + else: + return [tree] + + ######################################### + ## Button Callbacks + ######################################### + + def destroy(self, *e): + self._autostep = 0 + if self._top is None: + return + self._top.destroy() + self._top = None + + def reset(self, *e): + self._autostep = 0 + self._parser.initialize(self._sent) + self._lastoper1["text"] = "Reset Application" + self._lastoper2["text"] = "" + self._redraw() + + def autostep(self, *e): + if self._animation_frames.get() == 0: + self._animation_frames.set(2) + if self._autostep: + self._autostep = 0 + else: + self._autostep = 1 + self._step() + + def cancel_autostep(self, *e): + # self._autostep_button['text'] = 'Autostep' + self._autostep = 0 + + # Make sure to stop auto-stepping if we get any user input. + def step(self, *e): + self._autostep = 0 + self._step() + + def match(self, *e): + self._autostep = 0 + self._match() + + def expand(self, *e): + self._autostep = 0 + self._expand() + + def backtrack(self, *e): + self._autostep = 0 + self._backtrack() + + def _step(self): + if self._animating_lock: + return + + # Try expanding, matching, and backtracking (in that order) + if self._expand(): + pass + elif self._parser.untried_match() and self._match(): + pass + elif self._backtrack(): + pass + else: + self._lastoper1["text"] = "Finished" + self._lastoper2["text"] = "" + self._autostep = 0 + + # Check if we just completed a parse. + if self._parser.currently_complete(): + self._autostep = 0 + self._lastoper2["text"] += " [COMPLETE PARSE]" + + def _expand(self, *e): + if self._animating_lock: + return + old_frontier = self._parser.frontier() + rv = self._parser.expand() + if rv is not None: + self._lastoper1["text"] = "Expand:" + self._lastoper2["text"] = rv + self._prodlist.selection_clear(0, "end") + index = self._productions.index(rv) + self._prodlist.selection_set(index) + self._animate_expand(old_frontier[0]) + return True + else: + self._lastoper1["text"] = "Expand:" + self._lastoper2["text"] = "(all expansions tried)" + return False + + def _match(self, *e): + if self._animating_lock: + return + old_frontier = self._parser.frontier() + rv = self._parser.match() + if rv is not None: + self._lastoper1["text"] = "Match:" + self._lastoper2["text"] = rv + self._animate_match(old_frontier[0]) + return True + else: + self._lastoper1["text"] = "Match:" + self._lastoper2["text"] = "(failed)" + return False + + def _backtrack(self, *e): + if self._animating_lock: + return + if self._parser.backtrack(): + elt = self._parser.tree() + for i in self._parser.frontier()[0]: + elt = elt[i] + self._lastoper1["text"] = "Backtrack" + self._lastoper2["text"] = "" + if isinstance(elt, Tree): + self._animate_backtrack(self._parser.frontier()[0]) + else: + self._animate_match_backtrack(self._parser.frontier()[0]) + return True + else: + self._autostep = 0 + self._lastoper1["text"] = "Finished" + self._lastoper2["text"] = "" + return False + + def about(self, *e): + ABOUT = ( + "NLTK Recursive Descent Parser Application\n" + "Written by Edward Loper" + ) + TITLE = "About: Recursive Descent Parser Application" + try: + from tkinter.messagebox import Message + + Message(message=ABOUT, title=TITLE).show() + except Exception: + ShowText(self._top, TITLE, ABOUT) + + def help(self, *e): + self._autostep = 0 + # The default font's not very legible; try using 'fixed' instead. + try: + ShowText( + self._top, + "Help: Recursive Descent Parser Application", + (__doc__ or "").strip(), + width=75, + font="fixed", + ) + except Exception: + ShowText( + self._top, + "Help: Recursive Descent Parser Application", + (__doc__ or "").strip(), + width=75, + ) + + def postscript(self, *e): + self._autostep = 0 + self._cframe.print_to_file() + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this demo is created from a non-interactive program (e.g. + from a secript); otherwise, the demo will close as soon as + the script completes. + """ + if in_idle(): + return + self._top.mainloop(*args, **kwargs) + + def resize(self, size=None): + if size is not None: + self._size.set(size) + size = self._size.get() + self._font.configure(size=-(abs(size))) + self._boldfont.configure(size=-(abs(size))) + self._sysfont.configure(size=-(abs(size))) + self._bigfont.configure(size=-(abs(size + 2))) + self._redraw() + + ######################################### + ## Expand Production Selection + ######################################### + + def _toggle_grammar(self, *e): + if self._show_grammar.get(): + self._prodframe.pack( + fill="both", side="left", padx=2, after=self._feedbackframe + ) + self._lastoper1["text"] = "Show Grammar" + else: + self._prodframe.pack_forget() + self._lastoper1["text"] = "Hide Grammar" + self._lastoper2["text"] = "" + + # def toggle_grammar(self, *e): + # self._show_grammar = not self._show_grammar + # if self._show_grammar: + # self._prodframe.pack(fill='both', expand='y', side='left', + # after=self._feedbackframe) + # self._lastoper1['text'] = 'Show Grammar' + # else: + # self._prodframe.pack_forget() + # self._lastoper1['text'] = 'Hide Grammar' + # self._lastoper2['text'] = '' + + def _prodlist_select(self, event): + selection = self._prodlist.curselection() + if len(selection) != 1: + return + index = int(selection[0]) + old_frontier = self._parser.frontier() + production = self._parser.expand(self._productions[index]) + + if production: + self._lastoper1["text"] = "Expand:" + self._lastoper2["text"] = production + self._prodlist.selection_clear(0, "end") + self._prodlist.selection_set(index) + self._animate_expand(old_frontier[0]) + else: + # Reset the production selections. + self._prodlist.selection_clear(0, "end") + for prod in self._parser.expandable_productions(): + index = self._productions.index(prod) + self._prodlist.selection_set(index) + + ######################################### + ## Animation + ######################################### + + def _animate_expand(self, treeloc): + oldwidget = self._get(self._tree, treeloc) + oldtree = oldwidget.parent() + top = not isinstance(oldtree.parent(), TreeSegmentWidget) + + tree = self._parser.tree() + for i in treeloc: + tree = tree[i] + + widget = tree_to_treesegment( + self._canvas, + tree, + node_font=self._boldfont, + leaf_color="white", + tree_width=2, + tree_color="white", + node_color="white", + leaf_font=self._font, + ) + widget.label()["color"] = "#20a050" + + (oldx, oldy) = oldtree.label().bbox()[:2] + (newx, newy) = widget.label().bbox()[:2] + widget.move(oldx - newx, oldy - newy) + + if top: + self._cframe.add_widget(widget, 0, 5) + widget.move(30 - widget.label().bbox()[0], 0) + self._tree = widget + else: + oldtree.parent().replace_child(oldtree, widget) + + # Move the children over so they don't overlap. + # Line the children up in a strange way. + if widget.subtrees(): + dx = ( + oldx + + widget.label().width() / 2 + - widget.subtrees()[0].bbox()[0] / 2 + - widget.subtrees()[0].bbox()[2] / 2 + ) + for subtree in widget.subtrees(): + subtree.move(dx, 0) + + self._makeroom(widget) + + if top: + self._cframe.destroy_widget(oldtree) + else: + oldtree.destroy() + + colors = [ + "gray%d" % (10 * int(10 * x / self._animation_frames.get())) + for x in range(self._animation_frames.get(), 0, -1) + ] + + # Move the text string down, if necessary. + dy = widget.bbox()[3] + 30 - self._canvas.coords(self._textline)[1] + if dy > 0: + for twidget in self._textwidgets: + twidget.move(0, dy) + self._canvas.move(self._textline, 0, dy) + + self._animate_expand_frame(widget, colors) + + def _makeroom(self, treeseg): + """ + Make sure that no sibling tree bbox's overlap. + """ + parent = treeseg.parent() + if not isinstance(parent, TreeSegmentWidget): + return + + index = parent.subtrees().index(treeseg) + + # Handle siblings to the right + rsiblings = parent.subtrees()[index + 1 :] + if rsiblings: + dx = treeseg.bbox()[2] - rsiblings[0].bbox()[0] + 10 + for sibling in rsiblings: + sibling.move(dx, 0) + + # Handle siblings to the left + if index > 0: + lsibling = parent.subtrees()[index - 1] + dx = max(0, lsibling.bbox()[2] - treeseg.bbox()[0] + 10) + treeseg.move(dx, 0) + + # Keep working up the tree. + self._makeroom(parent) + + def _animate_expand_frame(self, widget, colors): + if len(colors) > 0: + self._animating_lock = 1 + widget["color"] = colors[0] + for subtree in widget.subtrees(): + if isinstance(subtree, TreeSegmentWidget): + subtree.label()["color"] = colors[0] + else: + subtree["color"] = colors[0] + self._top.after(50, self._animate_expand_frame, widget, colors[1:]) + else: + widget["color"] = "black" + for subtree in widget.subtrees(): + if isinstance(subtree, TreeSegmentWidget): + subtree.label()["color"] = "black" + else: + subtree["color"] = "black" + self._redraw_quick() + widget.label()["color"] = "black" + self._animating_lock = 0 + if self._autostep: + self._step() + + def _animate_backtrack(self, treeloc): + # Flash red first, if we're animating. + if self._animation_frames.get() == 0: + colors = [] + else: + colors = ["#a00000", "#000000", "#a00000"] + colors += [ + "gray%d" % (10 * int(10 * x / (self._animation_frames.get()))) + for x in range(1, self._animation_frames.get() + 1) + ] + + widgets = [self._get(self._tree, treeloc).parent()] + for subtree in widgets[0].subtrees(): + if isinstance(subtree, TreeSegmentWidget): + widgets.append(subtree.label()) + else: + widgets.append(subtree) + + self._animate_backtrack_frame(widgets, colors) + + def _animate_backtrack_frame(self, widgets, colors): + if len(colors) > 0: + self._animating_lock = 1 + for widget in widgets: + widget["color"] = colors[0] + self._top.after(50, self._animate_backtrack_frame, widgets, colors[1:]) + else: + for widget in widgets[0].subtrees(): + widgets[0].remove_child(widget) + widget.destroy() + self._redraw_quick() + self._animating_lock = 0 + if self._autostep: + self._step() + + def _animate_match_backtrack(self, treeloc): + widget = self._get(self._tree, treeloc) + node = widget.parent().label() + dy = (node.bbox()[3] - widget.bbox()[1] + 14) / max( + 1, self._animation_frames.get() + ) + self._animate_match_backtrack_frame(self._animation_frames.get(), widget, dy) + + def _animate_match(self, treeloc): + widget = self._get(self._tree, treeloc) + + dy = (self._textwidgets[0].bbox()[1] - widget.bbox()[3] - 10.0) / max( + 1, self._animation_frames.get() + ) + self._animate_match_frame(self._animation_frames.get(), widget, dy) + + def _animate_match_frame(self, frame, widget, dy): + if frame > 0: + self._animating_lock = 1 + widget.move(0, dy) + self._top.after(10, self._animate_match_frame, frame - 1, widget, dy) + else: + widget["color"] = "#006040" + self._redraw_quick() + self._animating_lock = 0 + if self._autostep: + self._step() + + def _animate_match_backtrack_frame(self, frame, widget, dy): + if frame > 0: + self._animating_lock = 1 + widget.move(0, dy) + self._top.after( + 10, self._animate_match_backtrack_frame, frame - 1, widget, dy + ) + else: + widget.parent().remove_child(widget) + widget.destroy() + self._animating_lock = 0 + if self._autostep: + self._step() + + def edit_grammar(self, *e): + CFGEditor(self._top, self._parser.grammar(), self.set_grammar) + + def set_grammar(self, grammar): + self._parser.set_grammar(grammar) + self._productions = list(grammar.productions()) + self._prodlist.delete(0, "end") + for production in self._productions: + self._prodlist.insert("end", (" %s" % production)) + + def edit_sentence(self, *e): + sentence = " ".join(self._sent) + title = "Edit Text" + instr = "Enter a new sentence to parse." + EntryDialog(self._top, sentence, instr, self.set_sentence, title) + + def set_sentence(self, sentence): + self._sent = sentence.split() # [XX] use tagged? + self.reset() + + +def app(): + """ + Create a recursive descent parser demo, using a simple grammar and + text. + """ + from nltk.grammar import CFG + + grammar = CFG.fromstring( + """ + # Grammatical productions. + S -> NP VP + NP -> Det N PP | Det N + VP -> V NP PP | V NP | V + PP -> P NP + # Lexical productions. + NP -> 'I' + Det -> 'the' | 'a' + N -> 'man' | 'park' | 'dog' | 'telescope' + V -> 'ate' | 'saw' + P -> 'in' | 'under' | 'with' + """ + ) + + sent = "the dog saw a man in the park".split() + + RecursiveDescentApp(grammar, sent).mainloop() + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/srparser_app.py b/nltk/app/srparser_app.py new file mode 100644 index 0000000..0fd219d --- /dev/null +++ b/nltk/app/srparser_app.py @@ -0,0 +1,937 @@ +# Natural Language Toolkit: Shift-Reduce Parser Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A graphical tool for exploring the shift-reduce parser. + +The shift-reduce parser maintains a stack, which records the structure +of the portion of the text that has been parsed. The stack is +initially empty. Its contents are shown on the left side of the main +canvas. + +On the right side of the main canvas is the remaining text. This is +the portion of the text which has not yet been considered by the +parser. + +The parser builds up a tree structure for the text using two +operations: + + - "shift" moves the first token from the remaining text to the top + of the stack. In the demo, the top of the stack is its right-hand + side. + - "reduce" uses a grammar production to combine the rightmost stack + elements into a single tree token. + +You can control the parser's operation by using the "shift" and +"reduce" buttons; or you can use the "step" button to let the parser +automatically decide which operation to apply. The parser uses the +following rules to decide which operation to apply: + + - Only shift if no reductions are available. + - If multiple reductions are available, then apply the reduction + whose CFG production is listed earliest in the grammar. + +The "reduce" button applies the reduction whose CFG production is +listed earliest in the grammar. There are two ways to manually choose +which reduction to apply: + + - Click on a CFG production from the list of available reductions, + on the left side of the main window. The reduction based on that + production will be applied to the top of the stack. + - Click on one of the stack elements. A popup window will appear, + containing all available reductions. Select one, and it will be + applied to the top of the stack. + +Note that reductions can only be applied to the top of the stack. + +Keyboard Shortcuts:: + [Space]\t Perform the next shift or reduce operation + [s]\t Perform a shift operation + [r]\t Perform a reduction operation + [Ctrl-z]\t Undo most recent operation + [Delete]\t Reset the parser + [g]\t Show/hide available production list + [Ctrl-a]\t Toggle animations + [h]\t Help + [Ctrl-p]\t Print + [q]\t Quit + +""" + +from tkinter import Button, Frame, IntVar, Label, Listbox, Menu, Scrollbar, Tk +from tkinter.font import Font + +from nltk.draw import CFGEditor, TreeSegmentWidget, tree_to_treesegment +from nltk.draw.util import CanvasFrame, EntryDialog, ShowText, TextWidget +from nltk.parse import SteppingShiftReduceParser +from nltk.tree import Tree +from nltk.util import in_idle + +""" +Possible future improvements: + - button/window to change and/or select text. Just pop up a window + with an entry, and let them modify the text; and then retokenize + it? Maybe give a warning if it contains tokens whose types are + not in the grammar. + - button/window to change and/or select grammar. Select from + several alternative grammars? Or actually change the grammar? If + the later, then I'd want to define nltk.draw.cfg, which would be + responsible for that. +""" + + +class ShiftReduceApp: + """ + A graphical tool for exploring the shift-reduce parser. The tool + displays the parser's stack and the remaining text, and allows the + user to control the parser's operation. In particular, the user + can shift tokens onto the stack, and can perform reductions on the + top elements of the stack. A "step" button simply steps through + the parsing process, performing the operations that + ``nltk.parse.ShiftReduceParser`` would use. + """ + + def __init__(self, grammar, sent, trace=0): + self._sent = sent + self._parser = SteppingShiftReduceParser(grammar, trace) + + # Set up the main window. + self._top = Tk() + self._top.title("Shift Reduce Parser Application") + + # Animations. animating_lock is a lock to prevent the demo + # from performing new operations while it's animating. + self._animating_lock = 0 + self._animate = IntVar(self._top) + self._animate.set(10) # = medium + + # The user can hide the grammar. + self._show_grammar = IntVar(self._top) + self._show_grammar.set(1) + + # Initialize fonts. + self._init_fonts(self._top) + + # Set up key bindings. + self._init_bindings() + + # Create the basic frames. + self._init_menubar(self._top) + self._init_buttons(self._top) + self._init_feedback(self._top) + self._init_grammar(self._top) + self._init_canvas(self._top) + + # A popup menu for reducing. + self._reduce_menu = Menu(self._canvas, tearoff=0) + + # Reset the demo, and set the feedback frame to empty. + self.reset() + self._lastoper1["text"] = "" + + ######################################### + ## Initialization Helpers + ######################################### + + def _init_fonts(self, root): + # See: + self._sysfont = Font(font=Button()["font"]) + root.option_add("*Font", self._sysfont) + + # TWhat's our font size (default=same as sysfont) + self._size = IntVar(root) + self._size.set(self._sysfont.cget("size")) + + self._boldfont = Font(family="helvetica", weight="bold", size=self._size.get()) + self._font = Font(family="helvetica", size=self._size.get()) + + def _init_grammar(self, parent): + # Grammar view. + self._prodframe = listframe = Frame(parent) + self._prodframe.pack(fill="both", side="left", padx=2) + self._prodlist_label = Label( + self._prodframe, font=self._boldfont, text="Available Reductions" + ) + self._prodlist_label.pack() + self._prodlist = Listbox( + self._prodframe, + selectmode="single", + relief="groove", + background="white", + foreground="#909090", + font=self._font, + selectforeground="#004040", + selectbackground="#c0f0c0", + ) + + self._prodlist.pack(side="right", fill="both", expand=1) + + self._productions = list(self._parser.grammar().productions()) + for production in self._productions: + self._prodlist.insert("end", (" %s" % production)) + self._prodlist.config(height=min(len(self._productions), 25)) + + # Add a scrollbar if there are more than 25 productions. + if 1: # len(self._productions) > 25: + listscroll = Scrollbar(self._prodframe, orient="vertical") + self._prodlist.config(yscrollcommand=listscroll.set) + listscroll.config(command=self._prodlist.yview) + listscroll.pack(side="left", fill="y") + + # If they select a production, apply it. + self._prodlist.bind("<>", self._prodlist_select) + + # When they hover over a production, highlight it. + self._hover = -1 + self._prodlist.bind("", self._highlight_hover) + self._prodlist.bind("", self._clear_hover) + + def _init_bindings(self): + # Quit + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + + # Ops (step, shift, reduce, undo) + self._top.bind("", self.step) + self._top.bind("", self.shift) + self._top.bind("", self.shift) + self._top.bind("", self.shift) + self._top.bind("", self.reduce) + self._top.bind("", self.reduce) + self._top.bind("", self.reduce) + self._top.bind("", self.reset) + self._top.bind("", self.undo) + self._top.bind("", self.undo) + self._top.bind("", self.undo) + self._top.bind("", self.undo) + self._top.bind("", self.undo) + + # Misc + self._top.bind("", self.postscript) + self._top.bind("", self.help) + self._top.bind("", self.help) + self._top.bind("", self.edit_grammar) + self._top.bind("", self.edit_sentence) + + # Animation speed control + self._top.bind("-", lambda e, a=self._animate: a.set(20)) + self._top.bind("=", lambda e, a=self._animate: a.set(10)) + self._top.bind("+", lambda e, a=self._animate: a.set(4)) + + def _init_buttons(self, parent): + # Set up the frames. + self._buttonframe = buttonframe = Frame(parent) + buttonframe.pack(fill="none", side="bottom") + Button( + buttonframe, + text="Step", + background="#90c0d0", + foreground="black", + command=self.step, + ).pack(side="left") + Button( + buttonframe, + text="Shift", + underline=0, + background="#90f090", + foreground="black", + command=self.shift, + ).pack(side="left") + Button( + buttonframe, + text="Reduce", + underline=0, + background="#90f090", + foreground="black", + command=self.reduce, + ).pack(side="left") + Button( + buttonframe, + text="Undo", + underline=0, + background="#f0a0a0", + foreground="black", + command=self.undo, + ).pack(side="left") + + def _init_menubar(self, parent): + menubar = Menu(parent) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Reset Parser", underline=0, command=self.reset, accelerator="Del" + ) + filemenu.add_command( + label="Print to Postscript", + underline=0, + command=self.postscript, + accelerator="Ctrl-p", + ) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + editmenu = Menu(menubar, tearoff=0) + editmenu.add_command( + label="Edit Grammar", + underline=5, + command=self.edit_grammar, + accelerator="Ctrl-g", + ) + editmenu.add_command( + label="Edit Text", + underline=5, + command=self.edit_sentence, + accelerator="Ctrl-t", + ) + menubar.add_cascade(label="Edit", underline=0, menu=editmenu) + + rulemenu = Menu(menubar, tearoff=0) + rulemenu.add_command( + label="Step", underline=1, command=self.step, accelerator="Space" + ) + rulemenu.add_separator() + rulemenu.add_command( + label="Shift", underline=0, command=self.shift, accelerator="Ctrl-s" + ) + rulemenu.add_command( + label="Reduce", underline=0, command=self.reduce, accelerator="Ctrl-r" + ) + rulemenu.add_separator() + rulemenu.add_command( + label="Undo", underline=0, command=self.undo, accelerator="Ctrl-u" + ) + menubar.add_cascade(label="Apply", underline=0, menu=rulemenu) + + viewmenu = Menu(menubar, tearoff=0) + viewmenu.add_checkbutton( + label="Show Grammar", + underline=0, + variable=self._show_grammar, + command=self._toggle_grammar, + ) + viewmenu.add_separator() + viewmenu.add_radiobutton( + label="Tiny", + variable=self._size, + underline=0, + value=10, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Small", + variable=self._size, + underline=0, + value=12, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Medium", + variable=self._size, + underline=0, + value=14, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Large", + variable=self._size, + underline=0, + value=18, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Huge", + variable=self._size, + underline=0, + value=24, + command=self.resize, + ) + menubar.add_cascade(label="View", underline=0, menu=viewmenu) + + animatemenu = Menu(menubar, tearoff=0) + animatemenu.add_radiobutton( + label="No Animation", underline=0, variable=self._animate, value=0 + ) + animatemenu.add_radiobutton( + label="Slow Animation", + underline=0, + variable=self._animate, + value=20, + accelerator="-", + ) + animatemenu.add_radiobutton( + label="Normal Animation", + underline=0, + variable=self._animate, + value=10, + accelerator="=", + ) + animatemenu.add_radiobutton( + label="Fast Animation", + underline=0, + variable=self._animate, + value=4, + accelerator="+", + ) + menubar.add_cascade(label="Animate", underline=1, menu=animatemenu) + + helpmenu = Menu(menubar, tearoff=0) + helpmenu.add_command(label="About", underline=0, command=self.about) + helpmenu.add_command( + label="Instructions", underline=0, command=self.help, accelerator="F1" + ) + menubar.add_cascade(label="Help", underline=0, menu=helpmenu) + + parent.config(menu=menubar) + + def _init_feedback(self, parent): + self._feedbackframe = feedbackframe = Frame(parent) + feedbackframe.pack(fill="x", side="bottom", padx=3, pady=3) + self._lastoper_label = Label( + feedbackframe, text="Last Operation:", font=self._font + ) + self._lastoper_label.pack(side="left") + lastoperframe = Frame(feedbackframe, relief="sunken", border=1) + lastoperframe.pack(fill="x", side="right", expand=1, padx=5) + self._lastoper1 = Label( + lastoperframe, foreground="#007070", background="#f0f0f0", font=self._font + ) + self._lastoper2 = Label( + lastoperframe, + anchor="w", + width=30, + foreground="#004040", + background="#f0f0f0", + font=self._font, + ) + self._lastoper1.pack(side="left") + self._lastoper2.pack(side="left", fill="x", expand=1) + + def _init_canvas(self, parent): + self._cframe = CanvasFrame( + parent, + background="white", + width=525, + closeenough=10, + border=2, + relief="sunken", + ) + self._cframe.pack(expand=1, fill="both", side="top", pady=2) + canvas = self._canvas = self._cframe.canvas() + + self._stackwidgets = [] + self._rtextwidgets = [] + self._titlebar = canvas.create_rectangle( + 0, 0, 0, 0, fill="#c0f0f0", outline="black" + ) + self._exprline = canvas.create_line(0, 0, 0, 0, dash=".") + self._stacktop = canvas.create_line(0, 0, 0, 0, fill="#408080") + size = self._size.get() + 4 + self._stacklabel = TextWidget( + canvas, "Stack", color="#004040", font=self._boldfont + ) + self._rtextlabel = TextWidget( + canvas, "Remaining Text", color="#004040", font=self._boldfont + ) + self._cframe.add_widget(self._stacklabel) + self._cframe.add_widget(self._rtextlabel) + + ######################################### + ## Main draw procedure + ######################################### + + def _redraw(self): + scrollregion = self._canvas["scrollregion"].split() + (cx1, cy1, cx2, cy2) = (int(c) for c in scrollregion) + + # Delete the old stack & rtext widgets. + for stackwidget in self._stackwidgets: + self._cframe.destroy_widget(stackwidget) + self._stackwidgets = [] + for rtextwidget in self._rtextwidgets: + self._cframe.destroy_widget(rtextwidget) + self._rtextwidgets = [] + + # Position the titlebar & exprline + (x1, y1, x2, y2) = self._stacklabel.bbox() + y = y2 - y1 + 10 + self._canvas.coords(self._titlebar, -5000, 0, 5000, y - 4) + self._canvas.coords(self._exprline, 0, y * 2 - 10, 5000, y * 2 - 10) + + # Position the titlebar labels.. + (x1, y1, x2, y2) = self._stacklabel.bbox() + self._stacklabel.move(5 - x1, 3 - y1) + (x1, y1, x2, y2) = self._rtextlabel.bbox() + self._rtextlabel.move(cx2 - x2 - 5, 3 - y1) + + # Draw the stack. + stackx = 5 + for tok in self._parser.stack(): + if isinstance(tok, Tree): + attribs = { + "tree_color": "#4080a0", + "tree_width": 2, + "node_font": self._boldfont, + "node_color": "#006060", + "leaf_color": "#006060", + "leaf_font": self._font, + } + widget = tree_to_treesegment(self._canvas, tok, **attribs) + widget.label()["color"] = "#000000" + else: + widget = TextWidget(self._canvas, tok, color="#000000", font=self._font) + widget.bind_click(self._popup_reduce) + self._stackwidgets.append(widget) + self._cframe.add_widget(widget, stackx, y) + stackx = widget.bbox()[2] + 10 + + # Draw the remaining text. + rtextwidth = 0 + for tok in self._parser.remaining_text(): + widget = TextWidget(self._canvas, tok, color="#000000", font=self._font) + self._rtextwidgets.append(widget) + self._cframe.add_widget(widget, rtextwidth, y) + rtextwidth = widget.bbox()[2] + 4 + + # Allow enough room to shift the next token (for animations) + if len(self._rtextwidgets) > 0: + stackx += self._rtextwidgets[0].width() + + # Move the remaining text to the correct location (keep it + # right-justified, when possible); and move the remaining text + # label, if necessary. + stackx = max(stackx, self._stacklabel.width() + 25) + rlabelwidth = self._rtextlabel.width() + 10 + if stackx >= cx2 - max(rtextwidth, rlabelwidth): + cx2 = stackx + max(rtextwidth, rlabelwidth) + for rtextwidget in self._rtextwidgets: + rtextwidget.move(4 + cx2 - rtextwidth, 0) + self._rtextlabel.move(cx2 - self._rtextlabel.bbox()[2] - 5, 0) + + midx = (stackx + cx2 - max(rtextwidth, rlabelwidth)) / 2 + self._canvas.coords(self._stacktop, midx, 0, midx, 5000) + (x1, y1, x2, y2) = self._stacklabel.bbox() + + # Set up binding to allow them to shift a token by dragging it. + if len(self._rtextwidgets) > 0: + + def drag_shift(widget, midx=midx, self=self): + if widget.bbox()[0] < midx: + self.shift() + else: + self._redraw() + + self._rtextwidgets[0].bind_drag(drag_shift) + self._rtextwidgets[0].bind_click(self.shift) + + # Draw the stack top. + self._highlight_productions() + + def _draw_stack_top(self, widget): + # hack.. + midx = widget.bbox()[2] + 50 + self._canvas.coords(self._stacktop, midx, 0, midx, 5000) + + def _highlight_productions(self): + # Highlight the productions that can be reduced. + self._prodlist.selection_clear(0, "end") + for prod in self._parser.reducible_productions(): + index = self._productions.index(prod) + self._prodlist.selection_set(index) + + ######################################### + ## Button Callbacks + ######################################### + + def destroy(self, *e): + if self._top is None: + return + self._top.destroy() + self._top = None + + def reset(self, *e): + self._parser.initialize(self._sent) + self._lastoper1["text"] = "Reset App" + self._lastoper2["text"] = "" + self._redraw() + + def step(self, *e): + if self.reduce(): + return True + elif self.shift(): + return True + else: + if list(self._parser.parses()): + self._lastoper1["text"] = "Finished:" + self._lastoper2["text"] = "Success" + else: + self._lastoper1["text"] = "Finished:" + self._lastoper2["text"] = "Failure" + + def shift(self, *e): + if self._animating_lock: + return + if self._parser.shift(): + tok = self._parser.stack()[-1] + self._lastoper1["text"] = "Shift:" + self._lastoper2["text"] = "%r" % tok + if self._animate.get(): + self._animate_shift() + else: + self._redraw() + return True + return False + + def reduce(self, *e): + if self._animating_lock: + return + production = self._parser.reduce() + if production: + self._lastoper1["text"] = "Reduce:" + self._lastoper2["text"] = "%s" % production + if self._animate.get(): + self._animate_reduce() + else: + self._redraw() + return production + + def undo(self, *e): + if self._animating_lock: + return + if self._parser.undo(): + self._redraw() + + def postscript(self, *e): + self._cframe.print_to_file() + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this demo is created from a non-interactive program (e.g. + from a secript); otherwise, the demo will close as soon as + the script completes. + """ + if in_idle(): + return + self._top.mainloop(*args, **kwargs) + + ######################################### + ## Menubar callbacks + ######################################### + + def resize(self, size=None): + if size is not None: + self._size.set(size) + size = self._size.get() + self._font.configure(size=-(abs(size))) + self._boldfont.configure(size=-(abs(size))) + self._sysfont.configure(size=-(abs(size))) + + # self._stacklabel['font'] = ('helvetica', -size-4, 'bold') + # self._rtextlabel['font'] = ('helvetica', -size-4, 'bold') + # self._lastoper_label['font'] = ('helvetica', -size) + # self._lastoper1['font'] = ('helvetica', -size) + # self._lastoper2['font'] = ('helvetica', -size) + # self._prodlist['font'] = ('helvetica', -size) + # self._prodlist_label['font'] = ('helvetica', -size-2, 'bold') + self._redraw() + + def help(self, *e): + # The default font's not very legible; try using 'fixed' instead. + try: + ShowText( + self._top, + "Help: Shift-Reduce Parser Application", + (__doc__ or "").strip(), + width=75, + font="fixed", + ) + except Exception: + ShowText( + self._top, + "Help: Shift-Reduce Parser Application", + (__doc__ or "").strip(), + width=75, + ) + + def about(self, *e): + ABOUT = "NLTK Shift-Reduce Parser Application\n" + "Written by Edward Loper" + TITLE = "About: Shift-Reduce Parser Application" + try: + from tkinter.messagebox import Message + + Message(message=ABOUT, title=TITLE).show() + except Exception: + ShowText(self._top, TITLE, ABOUT) + + def edit_grammar(self, *e): + CFGEditor(self._top, self._parser.grammar(), self.set_grammar) + + def set_grammar(self, grammar): + self._parser.set_grammar(grammar) + self._productions = list(grammar.productions()) + self._prodlist.delete(0, "end") + for production in self._productions: + self._prodlist.insert("end", (" %s" % production)) + + def edit_sentence(self, *e): + sentence = " ".join(self._sent) + title = "Edit Text" + instr = "Enter a new sentence to parse." + EntryDialog(self._top, sentence, instr, self.set_sentence, title) + + def set_sentence(self, sent): + self._sent = sent.split() # [XX] use tagged? + self.reset() + + ######################################### + ## Reduce Production Selection + ######################################### + + def _toggle_grammar(self, *e): + if self._show_grammar.get(): + self._prodframe.pack( + fill="both", side="left", padx=2, after=self._feedbackframe + ) + self._lastoper1["text"] = "Show Grammar" + else: + self._prodframe.pack_forget() + self._lastoper1["text"] = "Hide Grammar" + self._lastoper2["text"] = "" + + def _prodlist_select(self, event): + selection = self._prodlist.curselection() + if len(selection) != 1: + return + index = int(selection[0]) + production = self._parser.reduce(self._productions[index]) + if production: + self._lastoper1["text"] = "Reduce:" + self._lastoper2["text"] = "%s" % production + if self._animate.get(): + self._animate_reduce() + else: + self._redraw() + else: + # Reset the production selections. + self._prodlist.selection_clear(0, "end") + for prod in self._parser.reducible_productions(): + index = self._productions.index(prod) + self._prodlist.selection_set(index) + + def _popup_reduce(self, widget): + # Remove old commands. + productions = self._parser.reducible_productions() + if len(productions) == 0: + return + + self._reduce_menu.delete(0, "end") + for production in productions: + self._reduce_menu.add_command(label=str(production), command=self.reduce) + self._reduce_menu.post( + self._canvas.winfo_pointerx(), self._canvas.winfo_pointery() + ) + + ######################################### + ## Animations + ######################################### + + def _animate_shift(self): + # What widget are we shifting? + widget = self._rtextwidgets[0] + + # Where are we shifting from & to? + right = widget.bbox()[0] + if len(self._stackwidgets) == 0: + left = 5 + else: + left = self._stackwidgets[-1].bbox()[2] + 10 + + # Start animating. + dt = self._animate.get() + dx = (left - right) * 1.0 / dt + self._animate_shift_frame(dt, widget, dx) + + def _animate_shift_frame(self, frame, widget, dx): + if frame > 0: + self._animating_lock = 1 + widget.move(dx, 0) + self._top.after(10, self._animate_shift_frame, frame - 1, widget, dx) + else: + # but: stacktop?? + + # Shift the widget to the stack. + del self._rtextwidgets[0] + self._stackwidgets.append(widget) + self._animating_lock = 0 + + # Display the available productions. + self._draw_stack_top(widget) + self._highlight_productions() + + def _animate_reduce(self): + # What widgets are we shifting? + numwidgets = len(self._parser.stack()[-1]) # number of children + widgets = self._stackwidgets[-numwidgets:] + + # How far are we moving? + if isinstance(widgets[0], TreeSegmentWidget): + ydist = 15 + widgets[0].label().height() + else: + ydist = 15 + widgets[0].height() + + # Start animating. + dt = self._animate.get() + dy = ydist * 2.0 / dt + self._animate_reduce_frame(dt / 2, widgets, dy) + + def _animate_reduce_frame(self, frame, widgets, dy): + if frame > 0: + self._animating_lock = 1 + for widget in widgets: + widget.move(0, dy) + self._top.after(10, self._animate_reduce_frame, frame - 1, widgets, dy) + else: + del self._stackwidgets[-len(widgets) :] + for widget in widgets: + self._cframe.remove_widget(widget) + tok = self._parser.stack()[-1] + if not isinstance(tok, Tree): + raise ValueError() + label = TextWidget( + self._canvas, str(tok.label()), color="#006060", font=self._boldfont + ) + widget = TreeSegmentWidget(self._canvas, label, widgets, width=2) + (x1, y1, x2, y2) = self._stacklabel.bbox() + y = y2 - y1 + 10 + if not self._stackwidgets: + x = 5 + else: + x = self._stackwidgets[-1].bbox()[2] + 10 + self._cframe.add_widget(widget, x, y) + self._stackwidgets.append(widget) + + # Display the available productions. + self._draw_stack_top(widget) + self._highlight_productions() + + # # Delete the old widgets.. + # del self._stackwidgets[-len(widgets):] + # for widget in widgets: + # self._cframe.destroy_widget(widget) + # + # # Make a new one. + # tok = self._parser.stack()[-1] + # if isinstance(tok, Tree): + # attribs = {'tree_color': '#4080a0', 'tree_width': 2, + # 'node_font': bold, 'node_color': '#006060', + # 'leaf_color': '#006060', 'leaf_font':self._font} + # widget = tree_to_treesegment(self._canvas, tok.type(), + # **attribs) + # widget.node()['color'] = '#000000' + # else: + # widget = TextWidget(self._canvas, tok.type(), + # color='#000000', font=self._font) + # widget.bind_click(self._popup_reduce) + # (x1, y1, x2, y2) = self._stacklabel.bbox() + # y = y2-y1+10 + # if not self._stackwidgets: x = 5 + # else: x = self._stackwidgets[-1].bbox()[2] + 10 + # self._cframe.add_widget(widget, x, y) + # self._stackwidgets.append(widget) + + # self._redraw() + self._animating_lock = 0 + + ######################################### + ## Hovering. + ######################################### + + def _highlight_hover(self, event): + # What production are we hovering over? + index = self._prodlist.nearest(event.y) + if self._hover == index: + return + + # Clear any previous hover highlighting. + self._clear_hover() + + # If the production corresponds to an available reduction, + # highlight the stack. + selection = [int(s) for s in self._prodlist.curselection()] + if index in selection: + rhslen = len(self._productions[index].rhs()) + for stackwidget in self._stackwidgets[-rhslen:]: + if isinstance(stackwidget, TreeSegmentWidget): + stackwidget.label()["color"] = "#00a000" + else: + stackwidget["color"] = "#00a000" + + # Remember what production we're hovering over. + self._hover = index + + def _clear_hover(self, *event): + # Clear any previous hover highlighting. + if self._hover == -1: + return + self._hover = -1 + for stackwidget in self._stackwidgets: + if isinstance(stackwidget, TreeSegmentWidget): + stackwidget.label()["color"] = "black" + else: + stackwidget["color"] = "black" + + +def app(): + """ + Create a shift reduce parser app, using a simple grammar and + text. + """ + + from nltk.grammar import CFG, Nonterminal, Production + + nonterminals = "S VP NP PP P N Name V Det" + (S, VP, NP, PP, P, N, Name, V, Det) = (Nonterminal(s) for s in nonterminals.split()) + + productions = ( + # Syntactic Productions + Production(S, [NP, VP]), + Production(NP, [Det, N]), + Production(NP, [NP, PP]), + Production(VP, [VP, PP]), + Production(VP, [V, NP, PP]), + Production(VP, [V, NP]), + Production(PP, [P, NP]), + # Lexical Productions + Production(NP, ["I"]), + Production(Det, ["the"]), + Production(Det, ["a"]), + Production(N, ["man"]), + Production(V, ["saw"]), + Production(P, ["in"]), + Production(P, ["with"]), + Production(N, ["park"]), + Production(N, ["dog"]), + Production(N, ["statue"]), + Production(Det, ["my"]), + ) + + grammar = CFG(S, productions) + + # tokenize the sentence + sent = "my dog saw a man in the park with a statue".split() + + ShiftReduceApp(grammar, sent).mainloop() + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/wordfreq_app.py b/nltk/app/wordfreq_app.py new file mode 100644 index 0000000..09561d7 --- /dev/null +++ b/nltk/app/wordfreq_app.py @@ -0,0 +1,36 @@ +# Natural Language Toolkit: Wordfreq Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Sumukh Ghodke +# URL: +# For license information, see LICENSE.TXT + +from matplotlib import pylab + +from nltk.corpus import gutenberg +from nltk.text import Text + + +def plot_word_freq_dist(text): + fd = text.vocab() + + samples = [item for item, _ in fd.most_common(50)] + values = [fd[sample] for sample in samples] + values = [sum(values[: i + 1]) * 100.0 / fd.N() for i in range(len(values))] + pylab.title(text.name) + pylab.xlabel("Samples") + pylab.ylabel("Cumulative Percentage") + pylab.plot(values) + pylab.xticks(range(len(samples)), [str(s) for s in samples], rotation=90) + pylab.show() + + +def app(): + t1 = Text(gutenberg.words("melville-moby_dick.txt")) + plot_word_freq_dist(t1) + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/app/wordnet_app.py b/nltk/app/wordnet_app.py new file mode 100644 index 0000000..5a951a3 --- /dev/null +++ b/nltk/app/wordnet_app.py @@ -0,0 +1,1035 @@ +# Natural Language Toolkit: WordNet Browser Application +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Jussi Salmela +# Paul Bone +# URL: +# For license information, see LICENSE.TXT + +""" +A WordNet Browser application which launches the default browser +(if it is not already running) and opens a new tab with a connection +to http://localhost:port/ . It also starts an HTTP server on the +specified port and begins serving browser requests. The default +port is 8000. (For command-line help, run "python wordnet -h") +This application requires that the user's web browser supports +Javascript. + +BrowServer is a server for browsing the NLTK Wordnet database It first +launches a browser client to be used for browsing and then starts +serving the requests of that and maybe other clients + +Usage:: + + browserver.py -h + browserver.py [-s] [-p ] + +Options:: + + -h or --help + Display this help message. + + -l or --log-file + Logs messages to the given file, If this option is not specified + messages are silently dropped. + + -p or --port + Run the web server on this TCP port, defaults to 8000. + + -s or --server-mode + Do not start a web browser, and do not allow a user to + shutdown the server through the web interface. +""" +# TODO: throughout this package variable names and docstrings need +# modifying to be compliant with NLTK's coding standards. Tests also +# need to be develop to ensure this continues to work in the face of +# changes to other NLTK packages. + +import base64 +import copy +import getopt +import hmac +import html +import io +import os +import pickle +import secrets +import sys +import threading +import time +import webbrowser +from collections import defaultdict +from http.server import BaseHTTPRequestHandler, HTTPServer + +# Allow this program to run inside the NLTK source tree. +from sys import argv +from urllib.parse import parse_qs, unquote_plus + +from nltk.corpus import wordnet as wn +from nltk.corpus.reader.wordnet import Lemma, Synset +from nltk.picklesec import RestrictedUnpickler + +firstClient = True + +# Per-process secret token. It is embedded only in the browser's own "Shutdown" +# link and required by the shutdown route, so a cross-site page (which cannot +# read the link under the Same-Origin Policy) cannot forge a shutdown request +# (CWE-352). The loopback bind already blocks remote access (CWE-306). +_shutdown_token = secrets.token_urlsafe(32) + +# True if we're not also running a web browser. The value f server_mode +# gets set by demo(). +server_mode = None + +# If set this is a file object for writing log messages. +logfile = None + + +class MyServerHandler(BaseHTTPRequestHandler): + def do_HEAD(self): + self.send_head() + + def _shutdown_authorized(self): + """True only for a shutdown request carrying the per-process token. + + The token is generated once per server process and embedded only in the + browser's own Shutdown link, so a cross-site page cannot supply it; this + blocks CSRF-driven shutdown (CWE-352). + """ + token = parse_qs(self.path.partition("?")[2]).get("token", [""])[0] + return bool(_shutdown_token) and hmac.compare_digest(token, _shutdown_token) + + def do_GET(self): + global firstClient + sp = self.path[1:] + if unquote_plus(sp.partition("?")[0]) == "SHUTDOWN THE SERVER": + if server_mode: + page = "Server must be killed with SIGTERM." + type = "text/plain" + elif self._shutdown_authorized(): + print("Server shutting down!") + os._exit(0) + else: + # Refuse a token-less / cross-site shutdown request (CWE-352). + self.send_response(403) + self.send_header("Content-type", "text/plain") + self.end_headers() + self.wfile.write( + b"Forbidden: shutdown requires the per-process token " + b"from the browser's Shutdown link." + ) + return + + elif sp == "": # First request. + type = "text/html" + if not server_mode and firstClient: + firstClient = False + page = get_static_index_page(True) + else: + page = get_static_index_page(False) + word = "green" + + elif sp.endswith(".html"): # Trying to fetch a HTML file TODO: + type = "text/html" + usp = unquote_plus(sp) + if usp == "NLTK Wordnet Browser Database Info.html": + word = "* Database Info *" + if os.path.isfile(usp): + with open(usp) as infile: + page = infile.read() + else: + page = ( + (html_header % word) + "

The database info file:" + "

" + + usp + + "" + + "

was not found. Run this:" + + "

python dbinfo_html.py" + + "

to produce it." + + html_trailer + ) + else: + # Handle files here. + word = sp + try: + page = get_static_page_by_path(usp) + except FileNotFoundError: + page = "Internal error: Path for static page '%s' is unknown" % usp + # Set type to plain to prevent XSS by printing the path as HTML + type = "text/plain" + elif sp.startswith("search"): + # This doesn't seem to work with MWEs. + type = "text/html" + parts = (sp.split("?")[1]).split("&") + word = html.escape( + [ + p.split("=")[1].replace("+", " ") + for p in parts + if p.startswith("nextWord") + ][0] + ) + page, word = page_from_word(word) + elif sp.startswith("lookup_"): + # TODO add a variation of this that takes a non ecoded word or MWE. + type = "text/html" + sp = sp[len("lookup_") :] + page, word = page_from_href(sp) + elif sp == "start_page": + # if this is the first request we should display help + # information, and possibly set a default word. + type = "text/html" + page, word = page_from_word("wordnet") + else: + type = "text/plain" + page = "Could not parse request: '%s'" % sp + + # Send result. + self.send_head(type) + self.wfile.write(page.encode("utf8")) + + def send_head(self, type=None): + self.send_response(200) + self.send_header("Content-type", type) + self.end_headers() + + def log_message(self, format, *args): + global logfile + + if logfile: + logfile.write( + "%s - - [%s] %s\n" + % (self.address_string(), self.log_date_time_string(), format % args) + ) + + +def get_unique_counter_from_url(sp): + """ + Extract the unique counter from the URL if it has one. Otherwise return + null. + """ + pos = sp.rfind("%23") + if pos != -1: + return int(sp[(pos + 3) :]) + else: + return None + + +def wnb(port=8000, runBrowser=True, logfilename=None): + """ + Run NLTK Wordnet Browser Server. + + :param port: The port number for the server to listen on, defaults to + 8000 + :type port: int + + :param runBrowser: True to start a web browser and point it at the web + server. + :type runBrowser: bool + """ + # The webbrowser module is unpredictable, typically it blocks if it uses + # a console web browser, and doesn't block if it uses a GUI webbrowser, + # so we need to force it to have a clear correct behaviour. + # + # Normally the server should run for as long as the user wants. they + # should ideally be able to control this from the UI by closing the + # window or tab. Second best would be clicking a button to say + # 'Shutdown' that first shutsdown the server and closes the window or + # tab, or exits the text-mode browser. Both of these are unfreasable. + # + # The next best alternative is to start the server, have it close when + # it receives SIGTERM (default), and run the browser as well. The user + # may have to shutdown both programs. + # + # Since webbrowser may block, and the webserver will block, we must run + # them in separate threads. + # + global server_mode, logfile + server_mode = not runBrowser + + # Setup logging. + if logfilename: + try: + logfile = open(logfilename, "a", 1) # 1 means 'line buffering' + except OSError as e: + sys.stderr.write("Couldn't open %s for writing: %s", logfilename, e) + sys.exit(1) + else: + logfile = None + + # Compute URL and start web browser + url = "http://localhost:" + str(port) + + server_ready = None + browser_thread = None + + if runBrowser: + server_ready = threading.Event() + browser_thread = startBrowser(url, server_ready) + + # Start the server. Bind to localhost only to prevent remote access + # and unauthenticated shutdown via /SHUTDOWN%20THE%20SERVER. + server = HTTPServer(("127.0.0.1", port), MyServerHandler) + if logfile: + logfile.write("NLTK Wordnet browser server running serving: %s\n" % url) + if runBrowser: + server_ready.set() + + try: + server.serve_forever() + except KeyboardInterrupt: + pass + + if runBrowser: + browser_thread.join() + + if logfile: + logfile.close() + + +def startBrowser(url, server_ready): + def run(): + server_ready.wait() + time.sleep(1) # Wait a little bit more, there's still the chance of + # a race condition. + webbrowser.open(url, new=2, autoraise=1) + + t = threading.Thread(target=run) + t.start() + return t + + +##################################################################### +# Utilities +##################################################################### + + +""" +WordNet Browser Utilities. + +This provides a backend to both wxbrowse and browserver.py. +""" + +################################################################################ +# +# Main logic for wordnet browser. +# + + +# This is wrapped inside a function since wn is only available if the +# WordNet corpus is installed. +def _pos_tuples(): + return [ + (wn.NOUN, "N", "noun"), + (wn.VERB, "V", "verb"), + (wn.ADJ, "J", "adj"), + (wn.ADV, "R", "adv"), + ] + + +def _pos_match(pos_tuple): + """ + This function returns the complete pos tuple for the partial pos + tuple given to it. It attempts to match it against the first + non-null component of the given pos tuple. + """ + if pos_tuple[0] == "s": + pos_tuple = ("a", pos_tuple[1], pos_tuple[2]) + for n, x in enumerate(pos_tuple): + if x is not None: + break + for pt in _pos_tuples(): + if pt[n] == pos_tuple[n]: + return pt + return None + + +HYPONYM = 0 +HYPERNYM = 1 +CLASS_REGIONAL = 2 +PART_HOLONYM = 3 +PART_MERONYM = 4 +ATTRIBUTE = 5 +SUBSTANCE_HOLONYM = 6 +SUBSTANCE_MERONYM = 7 +MEMBER_HOLONYM = 8 +MEMBER_MERONYM = 9 +VERB_GROUP = 10 +INSTANCE_HYPONYM = 12 +INSTANCE_HYPERNYM = 13 +CAUSE = 14 +ALSO_SEE = 15 +SIMILAR = 16 +ENTAILMENT = 17 +ANTONYM = 18 +FRAMES = 19 +PERTAINYM = 20 + +CLASS_CATEGORY = 21 +CLASS_USAGE = 22 +CLASS_REGIONAL = 23 +CLASS_USAGE = 24 +CLASS_CATEGORY = 11 + +DERIVATIONALLY_RELATED_FORM = 25 + +INDIRECT_HYPERNYMS = 26 + + +def lemma_property(word, synset, func): + def flattern(l): + if l == []: + return [] + else: + return l[0] + flattern(l[1:]) + + return flattern([func(l) for l in synset.lemmas() if l.name == word]) + + +def rebuild_tree(orig_tree): + node = orig_tree[0] + children = orig_tree[1:] + return (node, [rebuild_tree(t) for t in children]) + + +def get_relations_data(word, synset): + """ + Get synset relations data for a synset. Note that this doesn't + yet support things such as full hyponym vs direct hyponym. + """ + if synset.pos() == wn.NOUN: + return ( + (HYPONYM, "Hyponyms", synset.hyponyms()), + (INSTANCE_HYPONYM, "Instance hyponyms", synset.instance_hyponyms()), + (HYPERNYM, "Direct hypernyms", synset.hypernyms()), + ( + INDIRECT_HYPERNYMS, + "Indirect hypernyms", + rebuild_tree(synset.tree(lambda x: x.hypernyms()))[1], + ), + # hypernyms', 'Sister terms', + (INSTANCE_HYPERNYM, "Instance hypernyms", synset.instance_hypernyms()), + # (CLASS_REGIONAL, ['domain term region'], ), + (PART_HOLONYM, "Part holonyms", synset.part_holonyms()), + (PART_MERONYM, "Part meronyms", synset.part_meronyms()), + (SUBSTANCE_HOLONYM, "Substance holonyms", synset.substance_holonyms()), + (SUBSTANCE_MERONYM, "Substance meronyms", synset.substance_meronyms()), + (MEMBER_HOLONYM, "Member holonyms", synset.member_holonyms()), + (MEMBER_MERONYM, "Member meronyms", synset.member_meronyms()), + (ATTRIBUTE, "Attributes", synset.attributes()), + (ANTONYM, "Antonyms", lemma_property(word, synset, lambda l: l.antonyms())), + ( + DERIVATIONALLY_RELATED_FORM, + "Derivationally related form", + lemma_property( + word, synset, lambda l: l.derivationally_related_forms() + ), + ), + ) + elif synset.pos() == wn.VERB: + return ( + (ANTONYM, "Antonym", lemma_property(word, synset, lambda l: l.antonyms())), + (HYPONYM, "Hyponym", synset.hyponyms()), + (HYPERNYM, "Direct hypernyms", synset.hypernyms()), + ( + INDIRECT_HYPERNYMS, + "Indirect hypernyms", + rebuild_tree(synset.tree(lambda x: x.hypernyms()))[1], + ), + (ENTAILMENT, "Entailments", synset.entailments()), + (CAUSE, "Causes", synset.causes()), + (ALSO_SEE, "Also see", synset.also_sees()), + (VERB_GROUP, "Verb Groups", synset.verb_groups()), + ( + DERIVATIONALLY_RELATED_FORM, + "Derivationally related form", + lemma_property( + word, synset, lambda l: l.derivationally_related_forms() + ), + ), + ) + elif synset.pos() == wn.ADJ or synset.pos() == wn.ADJ_SAT: + return ( + (ANTONYM, "Antonym", lemma_property(word, synset, lambda l: l.antonyms())), + (SIMILAR, "Similar to", synset.similar_tos()), + # Participle of verb - not supported by corpus + ( + PERTAINYM, + "Pertainyms", + lemma_property(word, synset, lambda l: l.pertainyms()), + ), + (ATTRIBUTE, "Attributes", synset.attributes()), + (ALSO_SEE, "Also see", synset.also_sees()), + ) + elif synset.pos() == wn.ADV: + # This is weird. adverbs such as 'quick' and 'fast' don't seem + # to have antonyms returned by the corpus.a + return ( + (ANTONYM, "Antonym", lemma_property(word, synset, lambda l: l.antonyms())), + ) + # Derived from adjective - not supported by corpus + else: + raise TypeError("Unhandled synset POS type: " + str(synset.pos())) + + +html_header = """ + + + + + +NLTK Wordnet Browser display of: %s + +""" +html_trailer = """ + + +""" + +explanation = """ +

Search Help

+
  • The display below the line is an example of the output the browser +shows you when you enter a search word. The search word was green.
  • +
  • The search result shows for different parts of speech the synsets +i.e. different meanings for the word.
  • +
  • All underlined texts are hypertext links. There are two types of links: +word links and others. Clicking a word link carries out a search for the word +in the Wordnet database.
  • +
  • Clicking a link of the other type opens a display section of data attached +to that link. Clicking that link a second time closes the section again.
  • +
  • Clicking S: opens a section showing the relations for that synset. +
  • +
  • Clicking on a relation name opens a section that displays the associated +synsets.
  • +
  • Type a search word in the Word field and start the search by the +Enter/Return key or click the Search button.
  • +
+
+""" + +# HTML oriented functions + + +def _bold(txt): + return "%s" % txt + + +def _center(txt): + return "
%s
" % txt + + +def _hlev(n, txt): + return "%s" % (n, txt, n) + + +def _italic(txt): + return "%s" % txt + + +def _li(txt): + return "
  • %s
  • " % txt + + +def pg(word, body): + """ + Return a HTML page of NLTK Browser format constructed from the + word and body + + :param word: The word that the body corresponds to + :type word: str + :param body: The HTML body corresponding to the word + :type body: str + :return: a HTML page for the word-body combination + :rtype: str + """ + return (html_header % word) + body + html_trailer + + +def _ul(txt): + return "
      " + txt + "
    " + + +def _abbc(txt): + """ + abbc = asterisks, breaks, bold, center + """ + return _center(_bold("
    " * 10 + "*" * 10 + " " + txt + " " + "*" * 10)) + + +full_hyponym_cont_text = _ul(_li(_italic("(has full hyponym continuation)"))) + "\n" + + +def _get_synset(synset_key): + """ + The synset key is the unique name of the synset, this can be + retrieved via synset.name() + """ + return wn.synset(synset_key) + + +def _collect_one_synset(word, synset, synset_relations): + """ + Returns the HTML string for one synset or word + + :param word: the current word + :type word: str + :param synset: a synset + :type synset: synset + :param synset_relations: information about which synset relations + to display. + :type synset_relations: dict(synset_key, set(relation_id)) + :return: The HTML string built for this synset + :rtype: str + """ + if isinstance(synset, tuple): # It's a word + raise NotImplementedError("word not supported by _collect_one_synset") + + typ = "S" + pos_tuple = _pos_match((synset.pos(), None, None)) + assert pos_tuple is not None, "pos_tuple is null: synset.pos(): %s" % synset.pos() + descr = pos_tuple[2] + ref = copy.deepcopy(Reference(word, synset_relations)) + ref.toggle_synset(synset) + synset_label = typ + ";" + if synset.name() in synset_relations: + synset_label = _bold(synset_label) + s = f"
  • {make_lookup_link(ref, synset_label)} ({descr}) " + + def format_lemma(w): + w = w.replace("_", " ") + if w.lower() == word: + return _bold(w) + else: + ref = Reference(w) + return make_lookup_link(ref, w) + + s += ", ".join(format_lemma(l.name()) for l in synset.lemmas()) + + gl = " ({}) {} ".format( + synset.definition(), + "; ".join('"%s"' % e for e in synset.examples()), + ) + return s + gl + _synset_relations(word, synset, synset_relations) + "
  • \n" + + +def _collect_all_synsets(word, pos, synset_relations=dict()): + """ + Return a HTML unordered list of synsets for the given word and + part of speech. + """ + return "
      %s\n
    \n" % "".join( + _collect_one_synset(word, synset, synset_relations) + for synset in wn.synsets(word, pos) + ) + + +def _synset_relations(word, synset, synset_relations): + """ + Builds the HTML string for the relations of a synset + + :param word: The current word + :type word: str + :param synset: The synset for which we're building the relations. + :type synset: Synset + :param synset_relations: synset keys and relation types for which to display relations. + :type synset_relations: dict(synset_key, set(relation_type)) + :return: The HTML for a synset's relations + :rtype: str + """ + + if synset.name() not in synset_relations: + return "" + ref = Reference(word, synset_relations) + + def relation_html(r): + if isinstance(r, Synset): + return make_lookup_link(Reference(r.lemma_names()[0]), r.lemma_names()[0]) + elif isinstance(r, Lemma): + return relation_html(r.synset()) + elif isinstance(r, tuple): + # It's probably a tuple containing a Synset and a list of + # similar tuples. This forms a tree of synsets. + return "{}\n
      {}
    \n".format( + relation_html(r[0]), + "".join("
  • %s
  • \n" % relation_html(sr) for sr in r[1]), + ) + else: + raise TypeError( + "r must be a synset, lemma or list, it was: type(r) = %s, r = %s" + % (type(r), r) + ) + + def make_synset_html(db_name, disp_name, rels): + synset_html = "%s\n" % make_lookup_link( + copy.deepcopy(ref).toggle_synset_relation(synset, db_name), + disp_name, + ) + + if db_name in ref.synset_relations[synset.name()]: + synset_html += "
      %s
    \n" % "".join( + "
  • %s
  • \n" % relation_html(r) for r in rels + ) + + return synset_html + + html = ( + "
      " + + "\n".join( + "
    • %s
    • " % make_synset_html(*rel_data) + for rel_data in get_relations_data(word, synset) + if rel_data[2] != [] + ) + + "
    " + ) + + return html + + +class Reference: + """ + A reference to a page that may be generated by page_word + """ + + def __init__(self, word, synset_relations=dict()): + """ + Build a reference to a new page. + + word is the word or words (separated by commas) for which to + search for synsets of + + synset_relations is a dictionary of synset keys to sets of + synset relation identifaiers to unfold a list of synset + relations for. + """ + self.word = word + self.synset_relations = synset_relations + + def encode(self): + """ + Encode this reference into a string to be used in a URL. + """ + # This uses a tuple rather than an object since the python + # pickle representation is much smaller and there is no need + # to represent the complete object. + string = pickle.dumps((self.word, self.synset_relations), -1) + return base64.urlsafe_b64encode(string).decode() + + @staticmethod + def decode(string): + """ + Decode a reference encoded with Reference.encode + """ + string = base64.urlsafe_b64decode(string.encode()) + word, synset_relations = RestrictedUnpickler(io.BytesIO(string)).load() + return Reference(word, synset_relations) + + def toggle_synset_relation(self, synset, relation): + """ + Toggle the display of the relations for the given synset and + relation type. + + This function will throw a KeyError if the synset is currently + not being displayed. + """ + if relation in self.synset_relations[synset.name()]: + self.synset_relations[synset.name()].remove(relation) + else: + self.synset_relations[synset.name()].add(relation) + + return self + + def toggle_synset(self, synset): + """ + Toggle displaying of the relation types for the given synset + """ + if synset.name() in self.synset_relations: + del self.synset_relations[synset.name()] + else: + self.synset_relations[synset.name()] = set() + + return self + + +def make_lookup_link(ref, label): + return f'{label}' + + +def page_from_word(word): + """ + Return a HTML page for the given word. + + :type word: str + :param word: The currently active word + :return: A tuple (page,word), where page is the new current HTML page + to be sent to the browser and + word is the new current word + :rtype: A tuple (str,str) + """ + return page_from_reference(Reference(word)) + + +def page_from_href(href): + """ + Returns a tuple of the HTML page built and the new current word + + :param href: The hypertext reference to be solved + :type href: str + :return: A tuple (page,word), where page is the new current HTML page + to be sent to the browser and + word is the new current word + :rtype: A tuple (str,str) + """ + return page_from_reference(Reference.decode(href)) + + +def page_from_reference(href): + """ + Returns a tuple of the HTML page built and the new current word + + :param href: The hypertext reference to be solved + :type href: str + :return: A tuple (page,word), where page is the new current HTML page + to be sent to the browser and + word is the new current word + :rtype: A tuple (str,str) + """ + word = href.word + pos_forms = defaultdict(list) + words = word.split(",") + words = [w for w in [w.strip().lower().replace(" ", "_") for w in words] if w != ""] + if len(words) == 0: + # No words were found. + return "", "Please specify a word to search for." + + # This looks up multiple words at once. This is probably not + # necessary and may lead to problems. + for w in words: + for pos in [wn.NOUN, wn.VERB, wn.ADJ, wn.ADV]: + form = wn.morphy(w, pos) + if form and form not in pos_forms[pos]: + pos_forms[pos].append(form) + body = "" + for pos, pos_str, name in _pos_tuples(): + if pos in pos_forms: + body += _hlev(3, name) + "\n" + for w in pos_forms[pos]: + # Not all words of exc files are in the database, skip + # to the next word if a KeyError is raised. + try: + body += _collect_all_synsets(w, pos, href.synset_relations) + except KeyError: + pass + if not body: + body = "The word or words '%s' were not found in the dictionary." % html.escape( + word + ) + return body, word + + +##################################################################### +# Static pages +##################################################################### + + +def get_static_page_by_path(path): + """ + Return a static HTML page from the path given. + """ + if path == "index_2.html": + return get_static_index_page(False) + elif path == "index.html": + return get_static_index_page(True) + elif path == "NLTK Wordnet Browser Database Info.html": + return "Display of Wordnet Database Statistics is not supported" + elif path == "upper_2.html": + return get_static_upper_page(False) + elif path == "upper.html": + return get_static_upper_page(True) + elif path == "web_help.html": + return get_static_web_help_page() + elif path == "wx_help.html": + return get_static_wx_help_page() + raise FileNotFoundError() + + +def get_static_web_help_page(): + """ + Return the static web help page. + """ + return """ + + + + + + NLTK Wordnet Browser display of: * Help * + + +

    NLTK Wordnet Browser Help

    +

    The NLTK Wordnet Browser is a tool to use in browsing the Wordnet database. It tries to behave like the Wordnet project's web browser but the difference is that the NLTK Wordnet Browser uses a local Wordnet database. +

    You are using the Javascript client part of the NLTK Wordnet BrowseServer. We assume your browser is in tab sheets enabled mode.

    +

    For background information on Wordnet, see the Wordnet project home page: https://wordnet.princeton.edu/. For more information on the NLTK project, see the project home: +https://www.nltk.org/. To get an idea of what the Wordnet version used by this browser includes choose Show Database Info from the View submenu.

    +

    Word search

    +

    The word to be searched is typed into the New Word field and the search started with Enter or by clicking the Search button. There is no uppercase/lowercase distinction: the search word is transformed to lowercase before the search.

    +

    In addition, the word does not have to be in base form. The browser tries to find the possible base form(s) by making certain morphological substitutions. Typing fLIeS as an obscure example gives one this. Click the previous link to see what this kind of search looks like and then come back to this page by using the Alt+LeftArrow key combination.

    +

    The result of a search is a display of one or more +synsets for every part of speech in which a form of the +search word was found to occur. A synset is a set of words +having the same sense or meaning. Each word in a synset that is +underlined is a hyperlink which can be clicked to trigger an +automatic search for that word.

    +

    Every synset has a hyperlink S: at the start of its +display line. Clicking that symbol shows you the name of every +relation that this synset is part of. Every relation name is a hyperlink that opens up a display for that relation. Clicking it another time closes the display again. Clicking another relation name on a line that has an opened relation closes the open relation and opens the clicked relation.

    +

    It is also possible to give two or more words or collocations to be searched at the same time separating them with a comma like this cheer up,clear up, for example. Click the previous link to see what this kind of search looks like and then come back to this page by using the Alt+LeftArrow key combination. As you could see the search result includes the synsets found in the same order than the forms were given in the search field.

    +

    +There are also word level (lexical) relations recorded in the Wordnet database. Opening this kind of relation displays lines with a hyperlink W: at their beginning. Clicking this link shows more info on the word in question.

    +

    The Buttons

    +

    The Search and Help buttons need no more explanation.

    +

    The Show Database Info button shows a collection of Wordnet database statistics.

    +

    The Shutdown the Server button is shown for the first client of the BrowServer program i.e. for the client that is automatically launched when the BrowServer is started but not for the succeeding clients in order to protect the server from accidental shutdowns. +

    + +""" + + +def get_static_welcome_message(): + """ + Get the static welcome page. + """ + return """ +

    Search Help

    +
    • The display below the line is an example of the output the browser +shows you when you enter a search word. The search word was green.
    • +
    • The search result shows for different parts of speech the synsets +i.e. different meanings for the word.
    • +
    • All underlined texts are hypertext links. There are two types of links: +word links and others. Clicking a word link carries out a search for the word +in the Wordnet database.
    • +
    • Clicking a link of the other type opens a display section of data attached +to that link. Clicking that link a second time closes the section again.
    • +
    • Clicking S: opens a section showing the relations for that synset.
    • +
    • Clicking on a relation name opens a section that displays the associated +synsets.
    • +
    • Type a search word in the Next Word field and start the search by the +Enter/Return key or click the Search button.
    • +
    +""" + + +def get_static_index_page(with_shutdown): + """ + Get the static index page. + """ + template = """ + + + + + NLTK Wordnet Browser + + + + + + + +""" + if with_shutdown: + upper_link = "upper.html" + else: + upper_link = "upper_2.html" + + return template % upper_link + + +def get_static_upper_page(with_shutdown): + """ + Return the upper frame page, + + If with_shutdown is True then a 'shutdown' button is also provided + to shutdown the server. + """ + template = """ + + + + + + Untitled Document + + +
    + Current Word:  + Next Word:  + +
    + Help + %s + + + +""" + if with_shutdown: + # Carry the per-process token so the shutdown route can tell this + # in-app click apart from a forged cross-site request (CWE-352). + shutdown_link = ( + 'Shutdown' % _shutdown_token + ) + else: + shutdown_link = "" + + return template % shutdown_link + + +def usage(): + """ + Display the command line help message. + """ + print(__doc__) + + +def app(): + # Parse and interpret options. + (opts, _) = getopt.getopt( + argv[1:], "l:p:sh", ["logfile=", "port=", "server-mode", "help"] + ) + port = 8000 + server_mode = False + help_mode = False + logfilename = None + for opt, value in opts: + if (opt == "-l") or (opt == "--logfile"): + logfilename = str(value) + elif (opt == "-p") or (opt == "--port"): + port = int(value) + elif (opt == "-s") or (opt == "--server-mode"): + server_mode = True + elif (opt == "-h") or (opt == "--help"): + help_mode = True + + if help_mode: + usage() + else: + wnb(port, not server_mode, logfilename) + + +if __name__ == "__main__": + app() + +__all__ = ["app"] diff --git a/nltk/book.py b/nltk/book.py new file mode 100644 index 0000000..1555945 --- /dev/null +++ b/nltk/book.py @@ -0,0 +1,213 @@ +# Natural Language Toolkit: Some texts for exploration in chapter 1 of the book +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# +# URL: +# For license information, see LICENSE.TXT + +from nltk.corpus import ( + genesis, + gutenberg, + inaugural, + nps_chat, + treebank, + webtext, + wordnet, +) +from nltk.probability import FreqDist +from nltk.text import Text +from nltk.util import bigrams + +print("*** Introductory Examples for the NLTK Book ***") +print("Loading text1, ..., text9 and sent1, ..., sent9") +print("Type the name of the text or sentence to view it.") +print("Type: 'texts()' or 'sents()' to list the materials.") + +text1 = Text(gutenberg.words("melville-moby_dick.txt")) +print("text1:", text1.name) + +text2 = Text(gutenberg.words("austen-sense.txt")) +print("text2:", text2.name) + +text3 = Text(genesis.words("english-kjv.txt"), name="The Book of Genesis") +print("text3:", text3.name) + +text4 = Text(inaugural.words(), name="Inaugural Address Corpus") +print("text4:", text4.name) + +text5 = Text(nps_chat.words(), name="Chat Corpus") +print("text5:", text5.name) + +text6 = Text(webtext.words("grail.txt"), name="Monty Python and the Holy Grail") +print("text6:", text6.name) + +text7 = Text(treebank.words(), name="Wall Street Journal") +print("text7:", text7.name) + +text8 = Text(webtext.words("singles.txt"), name="Personals Corpus") +print("text8:", text8.name) + +text9 = Text(gutenberg.words("chesterton-thursday.txt")) +print("text9:", text9.name) + + +def texts(): + print("text1:", text1.name) + print("text2:", text2.name) + print("text3:", text3.name) + print("text4:", text4.name) + print("text5:", text5.name) + print("text6:", text6.name) + print("text7:", text7.name) + print("text8:", text8.name) + print("text9:", text9.name) + + +sent1 = ["Call", "me", "Ishmael", "."] +sent2 = [ + "The", + "family", + "of", + "Dashwood", + "had", + "long", + "been", + "settled", + "in", + "Sussex", + ".", +] +sent3 = [ + "In", + "the", + "beginning", + "God", + "created", + "the", + "heaven", + "and", + "the", + "earth", + ".", +] +sent4 = [ + "Fellow", + "-", + "Citizens", + "of", + "the", + "Senate", + "and", + "of", + "the", + "House", + "of", + "Representatives", + ":", +] +sent5 = [ + "I", + "have", + "a", + "problem", + "with", + "people", + "PMing", + "me", + "to", + "lol", + "JOIN", +] +sent6 = [ + "SCENE", + "1", + ":", + "[", + "wind", + "]", + "[", + "clop", + "clop", + "clop", + "]", + "KING", + "ARTHUR", + ":", + "Whoa", + "there", + "!", +] +sent7 = [ + "Pierre", + "Vinken", + ",", + "61", + "years", + "old", + ",", + "will", + "join", + "the", + "board", + "as", + "a", + "nonexecutive", + "director", + "Nov.", + "29", + ".", +] +sent8 = [ + "25", + "SEXY", + "MALE", + ",", + "seeks", + "attrac", + "older", + "single", + "lady", + ",", + "for", + "discreet", + "encounters", + ".", +] +sent9 = [ + "THE", + "suburb", + "of", + "Saffron", + "Park", + "lay", + "on", + "the", + "sunset", + "side", + "of", + "London", + ",", + "as", + "red", + "and", + "ragged", + "as", + "a", + "cloud", + "of", + "sunset", + ".", +] + + +def sents(): + print("sent1:", " ".join(sent1)) + print("sent2:", " ".join(sent2)) + print("sent3:", " ".join(sent3)) + print("sent4:", " ".join(sent4)) + print("sent5:", " ".join(sent5)) + print("sent6:", " ".join(sent6)) + print("sent7:", " ".join(sent7)) + print("sent8:", " ".join(sent8)) + print("sent9:", " ".join(sent9)) diff --git a/nltk/ccg/__init__.py b/nltk/ccg/__init__.py new file mode 100644 index 0000000..15149aa --- /dev/null +++ b/nltk/ccg/__init__.py @@ -0,0 +1,34 @@ +# Natural Language Toolkit: Combinatory Categorial Grammar +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Graeme Gange +# URL: +# For license information, see LICENSE.TXT + +""" +Combinatory Categorial Grammar. + +For more information see nltk/doc/contrib/ccg/ccg.pdf +""" + +from nltk.ccg.chart import CCGChart, CCGChartParser, CCGEdge, CCGLeafEdge +from nltk.ccg.combinator import ( + BackwardApplication, + BackwardBx, + BackwardCombinator, + BackwardComposition, + BackwardSx, + BackwardT, + DirectedBinaryCombinator, + ForwardApplication, + ForwardCombinator, + ForwardComposition, + ForwardSubstitution, + ForwardT, + UndirectedBinaryCombinator, + UndirectedComposition, + UndirectedFunctionApplication, + UndirectedSubstitution, + UndirectedTypeRaise, +) +from nltk.ccg.lexicon import CCGLexicon diff --git a/nltk/ccg/api.py b/nltk/ccg/api.py new file mode 100644 index 0000000..2ccd407 --- /dev/null +++ b/nltk/ccg/api.py @@ -0,0 +1,368 @@ +# Natural Language Toolkit: CCG Categories +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Graeme Gange +# URL: +# For license information, see LICENSE.TXT + +from abc import ABCMeta, abstractmethod +from functools import total_ordering + +from nltk.internals import raise_unorderable_types + + +@total_ordering +class AbstractCCGCategory(metaclass=ABCMeta): + """ + Interface for categories in combinatory grammars. + """ + + @abstractmethod + def is_primitive(self): + """ + Returns true if the category is primitive. + """ + + @abstractmethod + def is_function(self): + """ + Returns true if the category is a function application. + """ + + @abstractmethod + def is_var(self): + """ + Returns true if the category is a variable. + """ + + @abstractmethod + def substitute(self, substitutions): + """ + Takes a set of (var, category) substitutions, and replaces every + occurrence of the variable with the corresponding category. + """ + + @abstractmethod + def can_unify(self, other): + """ + Determines whether two categories can be unified. + - Returns None if they cannot be unified + - Returns a list of necessary substitutions if they can. + """ + + # Utility functions: comparison, strings and hashing. + @abstractmethod + def __str__(self): + pass + + def __eq__(self, other): + return ( + self.__class__ is other.__class__ + and self._comparison_key == other._comparison_key + ) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, AbstractCCGCategory): + raise_unorderable_types("<", self, other) + if self.__class__ is other.__class__: + return self._comparison_key < other._comparison_key + else: + return self.__class__.__name__ < other.__class__.__name__ + + def __hash__(self): + try: + return self._hash + except AttributeError: + self._hash = hash(self._comparison_key) + return self._hash + + +class CCGVar(AbstractCCGCategory): + """ + Class representing a variable CCG category. + Used for conjunctions (and possibly type-raising, if implemented as a + unary rule). + """ + + _maxID = 0 + + def __init__(self, prim_only=False): + """Initialize a variable (selects a new identifier) + + :param prim_only: a boolean that determines whether the variable is + restricted to primitives + :type prim_only: bool + """ + self._id = self.new_id() + self._prim_only = prim_only + self._comparison_key = self._id + + @classmethod + def new_id(cls): + """ + A class method allowing generation of unique variable identifiers. + """ + cls._maxID = cls._maxID + 1 + return cls._maxID - 1 + + @classmethod + def reset_id(cls): + cls._maxID = 0 + + def is_primitive(self): + return False + + def is_function(self): + return False + + def is_var(self): + return True + + def substitute(self, substitutions): + """If there is a substitution corresponding to this variable, + return the substituted category. + """ + for var, cat in substitutions: + if var == self: + return cat + return self + + def can_unify(self, other): + """If the variable can be replaced with other + a substitution is returned. + """ + if other.is_primitive() or not self._prim_only: + return [(self, other)] + return None + + def id(self): + return self._id + + def __str__(self): + return "_var" + str(self._id) + + +@total_ordering +class Direction: + """ + Class representing the direction of a function application. + Also contains maintains information as to which combinators + may be used with the category. + """ + + def __init__(self, dir, restrictions): + self._dir = dir + if isinstance(restrictions, (tuple, list)): + restrictions = "".join(r for r in restrictions if r) + self._restrs = restrictions + self._comparison_key = (dir, tuple(restrictions)) + + # Testing the application direction + def is_forward(self): + return self._dir == "/" + + def is_backward(self): + return self._dir == "\\" + + def dir(self): + return self._dir + + def restrs(self): + """A list of restrictions on the combinators. + '.' denotes that permuting operations are disallowed + ',' denotes that function composition is disallowed + '_' denotes that the direction has variable restrictions. + (This is redundant in the current implementation of type-raising) + """ + return self._restrs + + def is_variable(self): + return self._restrs == "_" + + # Unification and substitution of variable directions. + # Used only if type-raising is implemented as a unary rule, as it + # must inherit restrictions from the argument category. + def can_unify(self, other): + if other.is_variable(): + return [("_", self.restrs())] + elif self.is_variable(): + return [("_", other.restrs())] + else: + if self.restrs() == other.restrs(): + return [] + return None + + def substitute(self, subs): + if not self.is_variable(): + return self + + for var, restrs in subs: + if var == "_": + return Direction(self._dir, restrs) + return self + + # Testing permitted combinators + def can_compose(self): + return "," not in self._restrs + + def can_cross(self): + return "." not in self._restrs + + def __eq__(self, other): + return ( + self.__class__ is other.__class__ + and self._comparison_key == other._comparison_key + ) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, Direction): + raise_unorderable_types("<", self, other) + if self.__class__ is other.__class__: + return self._comparison_key < other._comparison_key + else: + return self.__class__.__name__ < other.__class__.__name__ + + def __hash__(self): + try: + return self._hash + except AttributeError: + self._hash = hash(self._comparison_key) + return self._hash + + def __str__(self): + r_str = "" + for r in self._restrs: + r_str = r_str + "%s" % r + return f"{self._dir}{r_str}" + + # The negation operator reverses the direction of the application + def __neg__(self): + if self._dir == "/": + return Direction("\\", self._restrs) + else: + return Direction("/", self._restrs) + + +class PrimitiveCategory(AbstractCCGCategory): + """ + Class representing primitive categories. + Takes a string representation of the category, and a + list of strings specifying the morphological subcategories. + """ + + def __init__(self, categ, restrictions=[]): + self._categ = categ + self._restrs = restrictions + self._comparison_key = (categ, tuple(restrictions)) + + def is_primitive(self): + return True + + def is_function(self): + return False + + def is_var(self): + return False + + def restrs(self): + return self._restrs + + def categ(self): + return self._categ + + # Substitution does nothing to a primitive category + def substitute(self, subs): + return self + + # A primitive can be unified with a class of the same + # base category, given that the other category shares all + # of its subclasses, or with a variable. + def can_unify(self, other): + if not other.is_primitive(): + return None + if other.is_var(): + return [(other, self)] + if other.categ() == self.categ(): + for restr in self._restrs: + if restr not in other.restrs(): + return None + return [] + return None + + def __str__(self): + if self._restrs == []: + return "%s" % self._categ + restrictions = "[%s]" % ",".join(repr(r) for r in self._restrs) + return f"{self._categ}{restrictions}" + + +class FunctionalCategory(AbstractCCGCategory): + """ + Class that represents a function application category. + Consists of argument and result categories, together with + an application direction. + """ + + def __init__(self, res, arg, dir): + self._res = res + self._arg = arg + self._dir = dir + self._comparison_key = (arg, dir, res) + + def is_primitive(self): + return False + + def is_function(self): + return True + + def is_var(self): + return False + + # Substitution returns the category consisting of the + # substitution applied to each of its constituents. + def substitute(self, subs): + sub_res = self._res.substitute(subs) + sub_dir = self._dir.substitute(subs) + sub_arg = self._arg.substitute(subs) + return FunctionalCategory(sub_res, sub_arg, sub_dir) + + # A function can unify with another function, so long as its + # constituents can unify, or with an unrestricted variable. + def can_unify(self, other): + if other.is_var(): + return [(other, self)] + if other.is_function(): + sa = self._res.can_unify(other.res()) + sd = self._dir.can_unify(other.dir()) + + if sa is not None and sd is not None: + # Combine result and direction substitutions + base_subs = sa + sd + + # Apply all known substitutions to the arguments before unifying them + sb = self._arg.substitute(base_subs).can_unify( + other.arg().substitute(base_subs) + ) + + if sb is not None: + return base_subs + sb + return None + + # Constituent accessors + def arg(self): + return self._arg + + def res(self): + return self._res + + def dir(self): + return self._dir + + def __str__(self): + return f"({self._res}{self._dir}{self._arg})" diff --git a/nltk/ccg/chart.py b/nltk/ccg/chart.py new file mode 100644 index 0000000..a1ff599 --- /dev/null +++ b/nltk/ccg/chart.py @@ -0,0 +1,496 @@ +# Natural Language Toolkit: Combinatory Categorial Grammar +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Graeme Gange +# URL: +# For license information, see LICENSE.TXT + +""" +The lexicon is constructed by calling +``lexicon.fromstring()``. + +In order to construct a parser, you also need a rule set. +The standard English rules are provided in chart as +``chart.DefaultRuleSet``. + +The parser can then be constructed by calling, for example: +``parser = chart.CCGChartParser(, )`` + +Parsing is then performed by running +``parser.parse(.split())``. + +While this returns a list of trees, the default representation +of the produced trees is not very enlightening, particularly +given that it uses the same tree class as the CFG parsers. +It is probably better to call: +``chart.printCCGDerivation()`` +which should print a nice representation of the derivation. + +This entire process is shown far more clearly in the demonstration: +python chart.py +""" + +import itertools + +from nltk.ccg.combinator import * +from nltk.ccg.combinator import ( + BackwardApplication, + BackwardBx, + BackwardComposition, + BackwardSx, + BackwardT, + ForwardApplication, + ForwardComposition, + ForwardSubstitution, + ForwardT, +) +from nltk.ccg.lexicon import Token, fromstring +from nltk.ccg.logic import * +from nltk.parse import ParserI +from nltk.parse.chart import ( + MAX_PARSE_TREES, + AbstractChartRule, + Chart, + EdgeI, + _ParseTreeBudget, +) +from nltk.sem.logic import * +from nltk.tree import Tree + + +# Based on the EdgeI class from NLTK. +# A number of the properties of the EdgeI interface don't +# transfer well to CCGs, however. +class CCGEdge(EdgeI): + def __init__(self, span, categ, rule): + self._span = span + self._categ = categ + self._rule = rule + self._comparison_key = (span, categ, rule) + + # Accessors + def lhs(self): + return self._categ + + def span(self): + return self._span + + def start(self): + return self._span[0] + + def end(self): + return self._span[1] + + def length(self): + return self._span[1] - self.span[0] + + def rhs(self): + return () + + def dot(self): + return 0 + + def is_complete(self): + return True + + def is_incomplete(self): + return False + + def nextsym(self): + return None + + def categ(self): + return self._categ + + def rule(self): + return self._rule + + +class CCGLeafEdge(EdgeI): + """ + Class representing leaf edges in a CCG derivation. + """ + + def __init__(self, pos, token, leaf): + self._pos = pos + self._token = token + self._leaf = leaf + self._comparison_key = (pos, token.categ(), leaf) + + # Accessors + def lhs(self): + return self._token.categ() + + def span(self): + return (self._pos, self._pos + 1) + + def start(self): + return self._pos + + def end(self): + return self._pos + 1 + + def length(self): + return 1 + + def rhs(self): + return self._leaf + + def dot(self): + return 0 + + def is_complete(self): + return True + + def is_incomplete(self): + return False + + def nextsym(self): + return None + + def token(self): + return self._token + + def categ(self): + return self._token.categ() + + def leaf(self): + return self._leaf + + +class BinaryCombinatorRule(AbstractChartRule): + """ + Class implementing application of a binary combinator to a chart. + Takes the directed combinator to apply. + """ + + NUMEDGES = 2 + + def __init__(self, combinator): + self._combinator = combinator + + # Apply a combinator + def apply(self, chart, grammar, left_edge, right_edge): + # The left & right edges must be touching. + if not (left_edge.end() == right_edge.start()): + return + + # Check if the two edges are permitted to combine. + # If so, generate the corresponding edge. + if self._combinator.can_combine(left_edge.categ(), right_edge.categ()): + for res in self._combinator.combine(left_edge.categ(), right_edge.categ()): + new_edge = CCGEdge( + span=(left_edge.start(), right_edge.end()), + categ=res, + rule=self._combinator, + ) + if chart.insert(new_edge, (left_edge, right_edge)): + yield new_edge + + # The representation of the combinator (for printing derivations) + def __str__(self): + return "%s" % self._combinator + + +# Type-raising must be handled slightly differently to the other rules, as the +# resulting rules only span a single edge, rather than both edges. + + +class ForwardTypeRaiseRule(AbstractChartRule): + """ + Class for applying forward type raising + """ + + NUMEDGES = 2 + + def __init__(self): + self._combinator = ForwardT + + def apply(self, chart, grammar, left_edge, right_edge): + if not (left_edge.end() == right_edge.start()): + return + + for res in self._combinator.combine(left_edge.categ(), right_edge.categ()): + new_edge = CCGEdge(span=left_edge.span(), categ=res, rule=self._combinator) + if chart.insert(new_edge, (left_edge,)): + yield new_edge + + def __str__(self): + return "%s" % self._combinator + + +class BackwardTypeRaiseRule(AbstractChartRule): + """ + Class for applying backward type raising. + """ + + NUMEDGES = 2 + + def __init__(self): + self._combinator = BackwardT + + def apply(self, chart, grammar, left_edge, right_edge): + if not (left_edge.end() == right_edge.start()): + return + + for res in self._combinator.combine(left_edge.categ(), right_edge.categ()): + new_edge = CCGEdge(span=right_edge.span(), categ=res, rule=self._combinator) + if chart.insert(new_edge, (right_edge,)): + yield new_edge + + def __str__(self): + return "%s" % self._combinator + + +# Common sets of combinators used for English derivations. +ApplicationRuleSet = [ + BinaryCombinatorRule(ForwardApplication), + BinaryCombinatorRule(BackwardApplication), +] +CompositionRuleSet = [ + BinaryCombinatorRule(ForwardComposition), + BinaryCombinatorRule(BackwardComposition), + BinaryCombinatorRule(BackwardBx), +] +SubstitutionRuleSet = [ + BinaryCombinatorRule(ForwardSubstitution), + BinaryCombinatorRule(BackwardSx), +] +TypeRaiseRuleSet = [ForwardTypeRaiseRule(), BackwardTypeRaiseRule()] + +# The standard English rule set. +DefaultRuleSet = ( + ApplicationRuleSet + CompositionRuleSet + SubstitutionRuleSet + TypeRaiseRuleSet +) + + +class CCGChartParser(ParserI): + """ + Chart parser for CCGs. + Based largely on the ChartParser class from NLTK. + """ + + def __init__(self, lexicon, rules, trace=0): + self._lexicon = lexicon + self._rules = rules + self._trace = trace + + def lexicon(self): + return self._lexicon + + # Implements the CYK algorithm + def parse(self, tokens): + tokens = list(tokens) + chart = CCGChart(list(tokens)) + lex = self._lexicon + + # Initialize leaf edges. + for index in range(chart.num_leaves()): + for token in lex.categories(chart.leaf(index)): + new_edge = CCGLeafEdge(index, token, chart.leaf(index)) + chart.insert(new_edge, ()) + + # Select a span for the new edges + for span in range(2, chart.num_leaves() + 1): + for start in range(0, chart.num_leaves() - span + 1): + # Try all possible pairs of edges that could generate + # an edge for that span + for part in range(1, span): + lstart = start + mid = start + part + rend = start + span + + for left in chart.select(span=(lstart, mid)): + for right in chart.select(span=(mid, rend)): + # Generate all possible combinations of the two edges + for rule in self._rules: + edges_added_by_rule = 0 + for newedge in rule.apply(chart, lex, left, right): + edges_added_by_rule += 1 + + # Output the resulting parses + return chart.parses(lex.start()) + + +class CCGChart(Chart): + def __init__(self, tokens): + Chart.__init__(self, tokens) + + # Constructs the trees for a given parse. Unfortnunately, the parse trees need to be + # constructed slightly differently to those in the default Chart class, so it has to + # be reimplemented + def _trees(self, edge, complete, memo, tree_class, budget=None): + assert complete, "CCGChart cannot build incomplete trees" + + # Share the same node-construction budget as the base Chart so a + # highly-ambiguous CCG grammar cannot make tree extraction exponential + # either (CWE-770; CVE-2026-12886). + if budget is None: + budget = _ParseTreeBudget(MAX_PARSE_TREES) + + if edge in memo: + return memo[edge] + + if isinstance(edge, CCGLeafEdge): + budget.spend() + word = tree_class(edge.token(), [self._tokens[edge.start()]]) + leaf = tree_class((edge.token(), "Leaf"), [word]) + memo[edge] = [leaf] + return [leaf] + + memo[edge] = [] + trees = [] + + for cpl in self.child_pointer_lists(edge): + child_choices = [ + self._trees(cp, complete, memo, tree_class, budget) for cp in cpl + ] + for children in itertools.product(*child_choices): + budget.spend() + lhs = ( + Token( + self._tokens[edge.start() : edge.end()], + edge.lhs(), + compute_semantics(children, edge), + ), + str(edge.rule()), + ) + trees.append(tree_class(lhs, children)) + + memo[edge] = trees + return trees + + +def compute_semantics(children, edge): + if children[0].label()[0].semantics() is None: + return None + + if len(children) == 2: + if isinstance(edge.rule(), BackwardCombinator): + children = [children[1], children[0]] + + combinator = edge.rule()._combinator + function = children[0].label()[0].semantics() + argument = children[1].label()[0].semantics() + + if isinstance(combinator, UndirectedFunctionApplication): + return compute_function_semantics(function, argument) + elif isinstance(combinator, UndirectedComposition): + return compute_composition_semantics(function, argument) + elif isinstance(combinator, UndirectedSubstitution): + return compute_substitution_semantics(function, argument) + else: + raise AssertionError("Unsupported combinator '" + combinator + "'") + else: + return compute_type_raised_semantics(children[0].label()[0].semantics()) + + +# -------- +# Displaying derivations +# -------- +def printCCGDerivation(tree): + # Get the leaves and initial categories + leafcats = tree.pos() + leafstr = "" + catstr = "" + + # Construct a string with both the leaf word and corresponding + # category aligned. + for leaf, cat in leafcats: + str_cat = "%s" % cat + nextlen = 2 + max(len(leaf), len(str_cat)) + lcatlen = (nextlen - len(str_cat)) // 2 + rcatlen = lcatlen + (nextlen - len(str_cat)) % 2 + catstr += " " * lcatlen + str_cat + " " * rcatlen + lleaflen = (nextlen - len(leaf)) // 2 + rleaflen = lleaflen + (nextlen - len(leaf)) % 2 + leafstr += " " * lleaflen + leaf + " " * rleaflen + print(leafstr.rstrip()) + print(catstr.rstrip()) + + # Display the derivation steps + printCCGTree(0, tree) + + +# Prints the sequence of derivation steps. +def printCCGTree(lwidth, tree): + rwidth = lwidth + + # Is a leaf (word). + # Increment the span by the space occupied by the leaf. + if not isinstance(tree, Tree): + return 2 + lwidth + len(tree) + + # Find the width of the current derivation step + for child in tree: + rwidth = max(rwidth, printCCGTree(rwidth, child)) + + # Is a leaf node. + # Don't print anything, but account for the space occupied. + if not isinstance(tree.label(), tuple): + return max( + rwidth, 2 + lwidth + len("%s" % tree.label()), 2 + lwidth + len(tree[0]) + ) + + (token, op) = tree.label() + + if op == "Leaf": + return rwidth + + # Pad to the left with spaces, followed by a sequence of '-' + # and the derivation rule. + print(lwidth * " " + (rwidth - lwidth) * "-" + "%s" % op) + # Print the resulting category on a new line. + str_res = "%s" % (token.categ()) + if token.semantics() is not None: + str_res += " {" + str(token.semantics()) + "}" + respadlen = (rwidth - lwidth - len(str_res)) // 2 + lwidth + print(respadlen * " " + str_res) + return rwidth + + +### Demonstration code + +# Construct the lexicon +lex = fromstring( + """ + :- S, NP, N, VP # Primitive categories, S is the target primitive + + Det :: NP/N # Family of words + Pro :: NP + TV :: VP/NP + Modal :: (S\\NP)/VP # Backslashes need to be escaped + + I => Pro # Word -> Category mapping + you => Pro + + the => Det + + # Variables have the special keyword 'var' + # '.' prevents permutation + # ',' prevents composition + and => var\\.,var/.,var + + which => (N\\N)/(S/NP) + + will => Modal # Categories can be either explicit, or families. + might => Modal + + cook => TV + eat => TV + + mushrooms => N + parsnips => N + bacon => N + """ +) + + +def demo(): + parser = CCGChartParser(lex, DefaultRuleSet) + for parse in parser.parse("I might cook and eat the bacon".split()): + printCCGDerivation(parse) + + +if __name__ == "__main__": + demo() diff --git a/nltk/ccg/combinator.py b/nltk/ccg/combinator.py new file mode 100644 index 0000000..1d771bc --- /dev/null +++ b/nltk/ccg/combinator.py @@ -0,0 +1,340 @@ +# Natural Language Toolkit: Combinatory Categorial Grammar +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Graeme Gange +# URL: +# For license information, see LICENSE.TXT +""" +CCG Combinators +""" + +from abc import ABCMeta, abstractmethod + +from nltk.ccg.api import FunctionalCategory + + +class UndirectedBinaryCombinator(metaclass=ABCMeta): + """ + Abstract class for representing a binary combinator. + Merely defines functions for checking if the function and argument + are able to be combined, and what the resulting category is. + + Note that as no assumptions are made as to direction, the unrestricted + combinators can perform all backward, forward and crossed variations + of the combinators; these restrictions must be added in the rule + class. + """ + + @abstractmethod + def can_combine(self, function, argument): + pass + + @abstractmethod + def combine(self, function, argument): + pass + + +class DirectedBinaryCombinator(metaclass=ABCMeta): + """ + Wrapper for the undirected binary combinator. + It takes left and right categories, and decides which is to be + the function, and which the argument. + It then decides whether or not they can be combined. + """ + + @abstractmethod + def can_combine(self, left, right): + pass + + @abstractmethod + def combine(self, left, right): + pass + + +class ForwardCombinator(DirectedBinaryCombinator): + """ + Class representing combinators where the primary functor is on the left. + + Takes an undirected combinator, and a predicate which adds constraints + restricting the cases in which it may apply. + """ + + def __init__(self, combinator, predicate, suffix=""): + self._combinator = combinator + self._predicate = predicate + self._suffix = suffix + + def can_combine(self, left, right): + return self._combinator.can_combine(left, right) and self._predicate( + left, right + ) + + def combine(self, left, right): + yield from self._combinator.combine(left, right) + + def __str__(self): + return f">{self._combinator}{self._suffix}" + + +class BackwardCombinator(DirectedBinaryCombinator): + """ + The backward equivalent of the ForwardCombinator class. + """ + + def __init__(self, combinator, predicate, suffix=""): + self._combinator = combinator + self._predicate = predicate + self._suffix = suffix + + def can_combine(self, left, right): + return self._combinator.can_combine(right, left) and self._predicate( + left, right + ) + + def combine(self, left, right): + yield from self._combinator.combine(right, left) + + def __str__(self): + return f"<{self._combinator}{self._suffix}" + + +class UndirectedFunctionApplication(UndirectedBinaryCombinator): + """ + Class representing function application. + Implements rules of the form: + X/Y Y -> X (>) + And the corresponding backwards application rule + """ + + def can_combine(self, function, argument): + if not function.is_function(): + return False + + return function.arg().can_unify(argument) is not None + + def combine(self, function, argument): + if not function.is_function(): + return + + subs = function.arg().can_unify(argument) + if subs is None: + return + + yield function.res().substitute(subs) + + def __str__(self): + return "" + + +# Predicates for function application. + + +# Ensures the left functor takes an argument on the right +def forwardOnly(left, right): + return left.dir().is_forward() + + +# Ensures the right functor takes an argument on the left +def backwardOnly(left, right): + return right.dir().is_backward() + + +# Application combinator instances +ForwardApplication = ForwardCombinator(UndirectedFunctionApplication(), forwardOnly) +BackwardApplication = BackwardCombinator(UndirectedFunctionApplication(), backwardOnly) + + +class UndirectedComposition(UndirectedBinaryCombinator): + """ + Functional composition (harmonic) combinator. + Implements rules of the form + X/Y Y/Z -> X/Z (B>) + And the corresponding backwards and crossed variations. + """ + + def can_combine(self, function, argument): + # Can only combine two functions, and both functions must + # allow composition. + if not (function.is_function() and argument.is_function()): + return False + if function.dir().can_compose() and argument.dir().can_compose(): + return function.arg().can_unify(argument.res()) is not None + return False + + def combine(self, function, argument): + if not (function.is_function() and argument.is_function()): + return + if function.dir().can_compose() and argument.dir().can_compose(): + subs = function.arg().can_unify(argument.res()) + if subs is not None: + yield FunctionalCategory( + function.res().substitute(subs), + argument.arg().substitute(subs), + argument.dir(), + ) + + def __str__(self): + return "B" + + +# Predicates for restricting application of straight composition. +def bothForward(left, right): + return left.dir().is_forward() and right.dir().is_forward() + + +def bothBackward(left, right): + return left.dir().is_backward() and right.dir().is_backward() + + +# Predicates for crossed composition +def crossedDirs(left, right): + return left.dir().is_forward() and right.dir().is_backward() + + +def backwardBxConstraint(left, right): + # The functors must be crossed inwards + if not crossedDirs(left, right): + return False + # Permuting combinators must be allowed + if not left.dir().can_cross() and right.dir().can_cross(): + return False + # The resulting argument category is restricted to be primitive + return left.arg().is_primitive() + + +# Straight composition combinators +ForwardComposition = ForwardCombinator(UndirectedComposition(), forwardOnly) +BackwardComposition = BackwardCombinator(UndirectedComposition(), backwardOnly) + +# Backward crossed composition +BackwardBx = BackwardCombinator( + UndirectedComposition(), backwardBxConstraint, suffix="x" +) + + +class UndirectedSubstitution(UndirectedBinaryCombinator): + r""" + Substitution (permutation) combinator. + Implements rules of the form + Y/Z (X\Y)/Z -> X/Z ( N\N +def innermostFunction(categ): + while categ.res().is_function(): + categ = categ.res() + return categ + + +class UndirectedTypeRaise(UndirectedBinaryCombinator): + """ + Undirected combinator for type raising. + """ + + def can_combine(self, function, arg): + # The argument must be a function. + # The restriction that arg.res() must be a function + # merely reduces redundant type-raising; if arg.res() is + # primitive, we have: + # X Y\X =>((>) Y + # which is equivalent to + # X Y\X =>(<) Y + if not (arg.is_function() and arg.res().is_function()): + return False + + arg = innermostFunction(arg) + + # left, arg_categ are undefined! + subs = left.can_unify(arg_categ.arg()) + if subs is not None: + return True + return False + + def combine(self, function, arg): + if not ( + function.is_primitive() and arg.is_function() and arg.res().is_function() + ): + return + + # Type-raising matches only the innermost application. + arg = innermostFunction(arg) + + subs = function.can_unify(arg.arg()) + if subs is not None: + xcat = arg.res().substitute(subs) + yield FunctionalCategory( + xcat, FunctionalCategory(xcat, function, arg.dir()), -(arg.dir()) + ) + + def __str__(self): + return "T" + + +# Predicates for type-raising +# The direction of the innermost category must be towards +# the primary functor. +# The restriction that the variable must be primitive is not +# common to all versions of CCGs; some authors have other restrictions. +def forwardTConstraint(left, right): + arg = innermostFunction(right) + return arg.dir().is_backward() and arg.res().is_primitive() + + +def backwardTConstraint(left, right): + arg = innermostFunction(left) + return arg.dir().is_forward() and arg.res().is_primitive() + + +# Instances of type-raising combinators +ForwardT = ForwardCombinator(UndirectedTypeRaise(), forwardTConstraint) +BackwardT = BackwardCombinator(UndirectedTypeRaise(), backwardTConstraint) diff --git a/nltk/ccg/lexicon.py b/nltk/ccg/lexicon.py new file mode 100644 index 0000000..132595f --- /dev/null +++ b/nltk/ccg/lexicon.py @@ -0,0 +1,348 @@ +# Natural Language Toolkit: Combinatory Categorial Grammar +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Graeme Gange +# URL: +# For license information, see LICENSE.TXT +""" +CCG Lexicons +""" + +import re +from collections import defaultdict + +from nltk.ccg.api import CCGVar, Direction, FunctionalCategory, PrimitiveCategory +from nltk.internals import deprecated +from nltk.sem.logic import Expression + +# ------------ +# Regular expressions used for parsing components of the lexicon +# ------------ + +# Parses a primitive category and subscripts +PRIM_RE = re.compile(r"""([A-Za-z]+)(\[[A-Za-z,]+\])?""") + +# Separates the next primitive category from the remainder of the +# string +NEXTPRIM_RE = re.compile(r"""([A-Za-z]+(?:\[[A-Za-z,]+\])?)(.*)""") + +# Separates the next application operator from the remainder. +# The modifier slot also accepts `_`, marking a variable direction +# (e.g. `(S\_NP)/(S\_NP)` for a polymorphic adverb). +APP_RE = re.compile(r"""([\\/])([.,_]?)([.,]?)(.*)""") + +# Parses the definition of the right-hand side (rhs) of either a word or a family. +# The identifier and the arrow alternative ``[-=]+>`` both match ``-``/``=``, so the +# original ``([\S_]+)`` let the engine slide the identifier/arrow boundary across a +# long ``-``/``=`` run while re-scanning for the absent ``>`` from every position -- +# quadratic in the line length (CWE-1333). Anchoring the identifier's last character +# to be neither ``-`` nor ``=`` (``[^\s=-]``) fixes the boundary before any such run, +# so the arrow is tried once: the parse is linear, the usual whitespace-separated +# ``ident rhs`` form is unchanged, and the compact ``identrhs`` form is +# still accepted (and now splits sensibly, e.g. ``a-->b`` -> ``a``/``-->``/``b``). +LEX_RE = re.compile(r"""([\S_]*?[^\s=-])\s*(::|[-=]+>)\s*(.+)""", re.UNICODE) + +# Parses the right hand side that contains category and maybe semantic predicate +RHS_RE = re.compile(r"""([^{}]*[^ {}])\s*(\{[^}]+\})?""", re.UNICODE) + +# Parses the semantic predicate +SEMANTICS_RE = re.compile(r"""\{([^}]+)\}""", re.UNICODE) + +# Strips comments from a line +COMMENTS_RE = re.compile("""([^#]*)(?:#.*)?""") + + +class Token: + """ + Class representing a token. + + token => category {semantics} + e.g. eat => S\\var[pl]/var {\\x y.eat(x,y)} + + * `token` (string) + * `categ` (string) + * `semantics` (Expression) + """ + + def __init__(self, token, categ, semantics=None): + self._token = token + self._categ = categ + self._semantics = semantics + + def categ(self): + return self._categ + + def semantics(self): + return self._semantics + + def __str__(self): + semantics_str = "" + if self._semantics is not None: + semantics_str = " {" + str(self._semantics) + "}" + return "" + str(self._categ) + semantics_str + + def __cmp__(self, other): + if not isinstance(other, Token): + return -1 + return cmp((self._categ, self._semantics), other.categ(), other.semantics()) + + +class CCGLexicon: + """ + Class representing a lexicon for CCG grammars. + + * `primitives`: The list of primitive categories for the lexicon + * `families`: Families of categories + * `entries`: A mapping of words to possible categories + """ + + def __init__(self, start, primitives, families, entries): + self._start = PrimitiveCategory(start) + self._primitives = primitives + self._families = families + self._entries = entries + + def categories(self, word): + """ + Returns all the possible categories for a word + """ + return self._entries[word] + + def start(self): + """ + Return the target category for the parser + """ + return self._start + + def __str__(self): + """ + String representation of the lexicon. Used for debugging. + """ + string = "" + first = True + for ident in sorted(self._entries): + if not first: + string = string + "\n" + string = string + ident + " => " + + first = True + for cat in self._entries[ident]: + if not first: + string = string + " | " + else: + first = False + string = string + "%s" % cat + return string + + +# ----------- +# Parsing lexicons +# ----------- + + +def matchBrackets(string): + """ + Separate the contents matching the first set of brackets from the rest of + the input. + """ + rest = string[1:] + inside = "(" + + while rest != "" and not rest.startswith(")"): + if rest.startswith("("): + (part, rest) = matchBrackets(rest) + inside = inside + part + else: + inside = inside + rest[0] + rest = rest[1:] + if rest.startswith(")"): + return (inside + ")", rest[1:]) + raise AssertionError("Unmatched bracket in string '" + string + "'") + + +def nextCategory(string): + """ + Separate the string for the next portion of the category from the rest + of the string + """ + if string.startswith("("): + return matchBrackets(string) + return NEXTPRIM_RE.match(string).groups() + + +def parseApplication(app): + """ + Parse an application operator + """ + return Direction(app[0], app[1:]) + + +def parseSubscripts(subscr): + """ + Parse the subscripts for a primitive category + """ + if subscr: + return subscr[1:-1].split(",") + return [] + + +def parsePrimitiveCategory(chunks, primitives, families, var): + """ + Parse a primitive category + + If the primitive is the special category 'var', replace it with the + correct `CCGVar`. + """ + if chunks[0] == "var": + if chunks[1] is None: + if var is None: + var = CCGVar() + return (var, var) + + catstr = chunks[0] + if catstr in families: + (cat, cvar) = families[catstr] + if var is None: + var = cvar + else: + cat = cat.substitute([(cvar, var)]) + return (cat, var) + + if catstr in primitives: + subscrs = parseSubscripts(chunks[1]) + return (PrimitiveCategory(catstr, subscrs), var) + raise AssertionError( + "String '" + catstr + "' is neither a family nor primitive category." + ) + + +def augParseCategory(line, primitives, families, var=None): + """ + Parse a string representing a category, and returns a tuple with + (possibly) the CCG variable for the category + """ + (cat_string, rest) = nextCategory(line) + + if cat_string.startswith("("): + (res, var) = augParseCategory(cat_string[1:-1], primitives, families, var) + + else: + (res, var) = parsePrimitiveCategory( + PRIM_RE.match(cat_string).groups(), primitives, families, var + ) + + while rest != "": + app = APP_RE.match(rest).groups() + direction = parseApplication(app[0:3]) + rest = app[3] + + (cat_string, rest) = nextCategory(rest) + if cat_string.startswith("("): + (arg, var) = augParseCategory(cat_string[1:-1], primitives, families, var) + else: + (arg, var) = parsePrimitiveCategory( + PRIM_RE.match(cat_string).groups(), primitives, families, var + ) + res = FunctionalCategory(res, arg, direction) + + return (res, var) + + +def fromstring(lex_str, include_semantics=False): + """ + Convert string representation into a lexicon for CCGs. + """ + CCGVar.reset_id() + primitives = [] + families = {} + entries = defaultdict(list) + for line in lex_str.splitlines(): + # Strip comments and leading/trailing whitespace. + line = COMMENTS_RE.match(line).groups()[0].strip() + if line == "": + continue + + if line.startswith(":-"): + # A line of primitive categories. + # The first one is the target category + # ie, :- S, N, NP, VP + primitives = primitives + [ + prim.strip() for prim in line[2:].strip().split(",") + ] + else: + # Either a family definition, or a word definition + (ident, sep, rhs) = LEX_RE.match(line).groups() + (catstr, semantics_str) = RHS_RE.match(rhs).groups() + (cat, var) = augParseCategory(catstr, primitives, families) + + if sep == "::": + # Family definition + # ie, Det :: NP/N + families[ident] = (cat, var) + else: + semantics = None + if include_semantics is True: + if semantics_str is None: + raise AssertionError( + line + + " must contain semantics because include_semantics is set to True" + ) + else: + semantics = Expression.fromstring( + SEMANTICS_RE.match(semantics_str).groups()[0] + ) + # Word definition + # ie, which => (N\N)/(S/NP) + entries[ident].append(Token(ident, cat, semantics)) + return CCGLexicon(primitives[0], primitives, families, entries) + + +@deprecated("Use fromstring() instead.") +def parseLexicon(lex_str): + return fromstring(lex_str) + + +openccg_tinytiny = fromstring( + """ + # Rather minimal lexicon based on the openccg `tinytiny' grammar. + # Only incorporates a subset of the morphological subcategories, however. + :- S,NP,N # Primitive categories + Det :: NP/N # Determiners + Pro :: NP + IntransVsg :: S\\NP[sg] # Tensed intransitive verbs (singular) + IntransVpl :: S\\NP[pl] # Plural + TransVsg :: S\\NP[sg]/NP # Tensed transitive verbs (singular) + TransVpl :: S\\NP[pl]/NP # Plural + + the => NP[sg]/N[sg] + the => NP[pl]/N[pl] + + I => Pro + me => Pro + we => Pro + us => Pro + + book => N[sg] + books => N[pl] + + peach => N[sg] + peaches => N[pl] + + policeman => N[sg] + policemen => N[pl] + + boy => N[sg] + boys => N[pl] + + sleep => IntransVsg + sleep => IntransVpl + + eat => IntransVpl + eat => TransVpl + eats => IntransVsg + eats => TransVsg + + see => TransVpl + sees => TransVsg + """ +) diff --git a/nltk/ccg/logic.py b/nltk/ccg/logic.py new file mode 100644 index 0000000..d4355a8 --- /dev/null +++ b/nltk/ccg/logic.py @@ -0,0 +1,151 @@ +# Natural Language Toolkit: Combinatory Categorial Grammar +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Tanin Na Nakorn (@tanin) +# URL: +# For license information, see LICENSE.TXT +""" +Helper functions for CCG semantics computation +""" +import copy +import re + +from nltk.sem.logic import * + + +def barendregt_normalize(expr, counters=None): + """ + Canonicalizes variables while preserving NLTK's prefix-based typing. + Ensures alpha-equivalent formulas produce identical strings without capture. + Draws from standard pools (x,y,z for individuals; F,G for functors). + """ + if expr is None: + return None + + if counters is None: + expr = expr.simplify() + counters = {} + + if isinstance(expr, VariableBinderExpression): + # Extract the alphabetic prefix + match = re.match(r"^([A-Za-z_]+)", expr.variable.name) + base = match.group(1) if match else "v" + + # Group into pedagogical type pools to satisfy NLTK's type constraints + # while maintaining standard x, y, z readability. + if base in ("x", "y", "z", "w"): + category, pool = "ind", ["x", "y", "z"] + elif base in ("P", "Q", "R"): + category, pool = "pred", ["P", "Q", "R"] + elif base in ("F", "G", "H"): + category, pool = "func", ["F", "G"] + elif base == "e": + category, pool = "event", ["e"] + else: + category, pool = base, [base] + + if category not in counters: + counters[category] = 0 + + free_in_body = expr.term.free() - {expr.variable} + + while True: + idx = counters[category] + pool_var = pool[idx % len(pool)] + suffix = idx // len(pool) + new_name = f"{pool_var}{suffix if suffix > 0 else ''}" + new_var = Variable(new_name) + counters[category] += 1 + + # Prevent capture with strictly external free variables + if new_var not in free_in_body: + break + + safe_expr = expr.alpha_convert(new_var) + return safe_expr.__class__( + safe_expr.variable, barendregt_normalize(safe_expr.term, counters) + ) + + elif isinstance(expr, ApplicationExpression): + return ApplicationExpression( + barendregt_normalize(expr.function, counters), + barendregt_normalize(expr.argument, counters), + ) + + elif isinstance(expr, BooleanExpression): + return expr.__class__( + barendregt_normalize(expr.first, counters), + barendregt_normalize(expr.second, counters), + ) + + elif isinstance(expr, NegatedExpression): + return NegatedExpression(barendregt_normalize(expr.term, counters)) + + elif isinstance(expr, EqualityExpression): + return expr.__class__( + barendregt_normalize(expr.first, counters), + barendregt_normalize(expr.second, counters), + ) + + return expr + + +def compute_function_semantics(function, argument): + if function is None or argument is None: + return None + return barendregt_normalize(ApplicationExpression(function, argument)) + + +def compute_type_raised_semantics(semantics): + if semantics is None: + return None + core = unique_variable(pattern=Variable("F")) + # Strictly pure type-raising: \F.F(semantics) + return barendregt_normalize( + LambdaExpression( + core, + ApplicationExpression(VariableExpression(core), copy.deepcopy(semantics)), + ) + ) + + +def compute_composition_semantics(function, argument): + if function is None or argument is None: + return None + assert isinstance( + argument, LambdaExpression + ), f"`{argument}` must be a lambda expression" + + # Extract the type pattern directly from the argument + v = unique_variable(pattern=argument.variable) + return barendregt_normalize( + LambdaExpression( + v, + ApplicationExpression( + function, ApplicationExpression(argument, VariableExpression(v)) + ), + ) + ) + + +def compute_substitution_semantics(function, argument): + if function is None or argument is None: + return None + assert isinstance(function, LambdaExpression) and isinstance( + function.term, LambdaExpression + ), f"`{function}` must be a lambda expression with 2 arguments" + assert isinstance( + argument, LambdaExpression + ), f"`{argument}` must be a lambda expression" + + # Copilot Fix: Extract the type pattern directly from the function + x_var = unique_variable(pattern=function.variable) + return barendregt_normalize( + LambdaExpression( + x_var, + ApplicationExpression( + ApplicationExpression(function, VariableExpression(x_var)), + ApplicationExpression(argument, VariableExpression(x_var)), + ), + ) + ) diff --git a/nltk/chat/__init__.py b/nltk/chat/__init__.py new file mode 100644 index 0000000..e60fc49 --- /dev/null +++ b/nltk/chat/__init__.py @@ -0,0 +1,48 @@ +# Natural Language Toolkit: Chatbots +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +# Based on an Eliza implementation by Joe Strout , +# Jeff Epler and Jez Higgins . + +""" +A class for simple chatbots. These perform simple pattern matching on sentences +typed by users, and respond with automatically generated sentences. + +These chatbots may not work using the windows command line or the +windows IDLE GUI. +""" + +from nltk.chat.eliza import eliza_chat +from nltk.chat.iesha import iesha_chat +from nltk.chat.rude import rude_chat +from nltk.chat.suntsu import suntsu_chat +from nltk.chat.util import Chat +from nltk.chat.zen import zen_chat + +bots = [ + (eliza_chat, "Eliza (psycho-babble)"), + (iesha_chat, "Iesha (teen anime junky)"), + (rude_chat, "Rude (abusive bot)"), + (suntsu_chat, "Suntsu (Chinese sayings)"), + (zen_chat, "Zen (gems of wisdom)"), +] + + +def chatbots(): + print("Which chatbot would you like to talk to?") + botcount = len(bots) + for i in range(botcount): + print(" %d: %s" % (i + 1, bots[i][1])) + while True: + choice = input(f"\nEnter a number in the range 1-{botcount}: ").strip() + if choice.isdigit() and (int(choice) - 1) in range(botcount): + break + else: + print(" Error: bad chatbot number") + + chatbot = bots[int(choice) - 1][0] + chatbot() diff --git a/nltk/chat/eliza.py b/nltk/chat/eliza.py new file mode 100644 index 0000000..f48e123 --- /dev/null +++ b/nltk/chat/eliza.py @@ -0,0 +1,337 @@ +# Natural Language Toolkit: Eliza +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +# Based on an Eliza implementation by Joe Strout , +# Jeff Epler and Jez Higgins . + +# a translation table used to convert things you say into things the +# computer says back, e.g. "I am" --> "you are" + +from nltk.chat.util import Chat, reflections + +# a table of response pairs, where each pair consists of a +# regular expression, and a list of possible responses, +# with group-macros labelled as %1, %2. + +pairs = ( + ( + r"I need (.*)", + ( + "Why do you need %1?", + "Would it really help you to get %1?", + "Are you sure you need %1?", + ), + ), + ( + r"Why don\'t you (.*)", + ( + "Do you really think I don't %1?", + "Perhaps eventually I will %1.", + "Do you really want me to %1?", + ), + ), + ( + r"Why can\'t I (.*)", + ( + "Do you think you should be able to %1?", + "If you could %1, what would you do?", + "I don't know -- why can't you %1?", + "Have you really tried?", + ), + ), + ( + r"I can\'t (.*)", + ( + "How do you know you can't %1?", + "Perhaps you could %1 if you tried.", + "What would it take for you to %1?", + ), + ), + ( + r"I am (.*)", + ( + "Did you come to me because you are %1?", + "How long have you been %1?", + "How do you feel about being %1?", + ), + ), + ( + r"I\'m (.*)", + ( + "How does being %1 make you feel?", + "Do you enjoy being %1?", + "Why do you tell me you're %1?", + "Why do you think you're %1?", + ), + ), + ( + r"Are you (.*)", + ( + "Why does it matter whether I am %1?", + "Would you prefer it if I were not %1?", + "Perhaps you believe I am %1.", + "I may be %1 -- what do you think?", + ), + ), + ( + r"What (.*)", + ( + "Why do you ask?", + "How would an answer to that help you?", + "What do you think?", + ), + ), + ( + r"How (.*)", + ( + "How do you suppose?", + "Perhaps you can answer your own question.", + "What is it you're really asking?", + ), + ), + ( + r"Because (.*)", + ( + "Is that the real reason?", + "What other reasons come to mind?", + "Does that reason apply to anything else?", + "If %1, what else must be true?", + ), + ), + ( + r"(.*) sorry (.*)", + ( + "There are many times when no apology is needed.", + "What feelings do you have when you apologize?", + ), + ), + ( + r"Hello(.*)", + ( + "Hello... I'm glad you could drop by today.", + "Hi there... how are you today?", + "Hello, how are you feeling today?", + ), + ), + ( + r"I think (.*)", + ("Do you doubt %1?", "Do you really think so?", "But you're not sure %1?"), + ), + ( + r"(.*) friend (.*)", + ( + "Tell me more about your friends.", + "When you think of a friend, what comes to mind?", + "Why don't you tell me about a childhood friend?", + ), + ), + (r"Yes", ("You seem quite sure.", "OK, but can you elaborate a bit?")), + ( + r"(.*) computer(.*)", + ( + "Are you really talking about me?", + "Does it seem strange to talk to a computer?", + "How do computers make you feel?", + "Do you feel threatened by computers?", + ), + ), + ( + r"Is it (.*)", + ( + "Do you think it is %1?", + "Perhaps it's %1 -- what do you think?", + "If it were %1, what would you do?", + "It could well be that %1.", + ), + ), + ( + r"It is (.*)", + ( + "You seem very certain.", + "If I told you that it probably isn't %1, what would you feel?", + ), + ), + ( + r"Can you (.*)", + ( + "What makes you think I can't %1?", + "If I could %1, then what?", + "Why do you ask if I can %1?", + ), + ), + ( + r"Can I (.*)", + ( + "Perhaps you don't want to %1.", + "Do you want to be able to %1?", + "If you could %1, would you?", + ), + ), + ( + r"You are (.*)", + ( + "Why do you think I am %1?", + "Does it please you to think that I'm %1?", + "Perhaps you would like me to be %1.", + "Perhaps you're really talking about yourself?", + ), + ), + ( + r"You\'re (.*)", + ( + "Why do you say I am %1?", + "Why do you think I am %1?", + "Are we talking about you, or me?", + ), + ), + ( + r"I don\'t (.*)", + ("Don't you really %1?", "Why don't you %1?", "Do you want to %1?"), + ), + ( + r"I feel (.*)", + ( + "Good, tell me more about these feelings.", + "Do you often feel %1?", + "When do you usually feel %1?", + "When you feel %1, what do you do?", + ), + ), + ( + r"I have (.*)", + ( + "Why do you tell me that you've %1?", + "Have you really %1?", + "Now that you have %1, what will you do next?", + ), + ), + ( + r"I would (.*)", + ( + "Could you explain why you would %1?", + "Why would you %1?", + "Who else knows that you would %1?", + ), + ), + ( + r"Is there (.*)", + ( + "Do you think there is %1?", + "It's likely that there is %1.", + "Would you like there to be %1?", + ), + ), + ( + r"My (.*)", + ( + "I see, your %1.", + "Why do you say that your %1?", + "When your %1, how do you feel?", + ), + ), + ( + r"You (.*)", + ( + "We should be discussing you, not me.", + "Why do you say that about me?", + "Why do you care whether I %1?", + ), + ), + (r"Why (.*)", ("Why don't you tell me the reason why %1?", "Why do you think %1?")), + ( + r"I want (.*)", + ( + "What would it mean to you if you got %1?", + "Why do you want %1?", + "What would you do if you got %1?", + "If you got %1, then what would you do?", + ), + ), + ( + r"(.*) mother(.*)", + ( + "Tell me more about your mother.", + "What was your relationship with your mother like?", + "How do you feel about your mother?", + "How does this relate to your feelings today?", + "Good family relations are important.", + ), + ), + ( + r"(.*) father(.*)", + ( + "Tell me more about your father.", + "How did your father make you feel?", + "How do you feel about your father?", + "Does your relationship with your father relate to your feelings today?", + "Do you have trouble showing affection with your family?", + ), + ), + ( + r"(.*) child(.*)", + ( + "Did you have close friends as a child?", + "What is your favorite childhood memory?", + "Do you remember any dreams or nightmares from childhood?", + "Did the other children sometimes tease you?", + "How do you think your childhood experiences relate to your feelings today?", + ), + ), + ( + r"(.*)\?", + ( + "Why do you ask that?", + "Please consider whether you can answer your own question.", + "Perhaps the answer lies within yourself?", + "Why don't you tell me?", + ), + ), + ( + r"quit", + ( + "Thank you for talking with me.", + "Good-bye.", + "Thank you, that will be $150. Have a good day!", + ), + ), + ( + r"(.*)", + ( + "Please tell me more.", + "Let's change focus a bit... Tell me about your family.", + "Can you elaborate on that?", + "Why do you say that %1?", + "I see.", + "Very interesting.", + "%1.", + "I see. And what does that tell you?", + "How does that make you feel?", + "How do you feel when you say that?", + ), + ), +) + +eliza_chatbot = Chat(pairs, reflections) + + +def eliza_chat(): + print("Therapist\n---------") + print("Talk to the program by typing in plain English, using normal upper-") + print('and lower-case letters and punctuation. Enter "quit" when done.') + print("=" * 72) + print("Hello. How are you feeling today?") + + eliza_chatbot.converse() + + +def demo(): + eliza_chat() + + +if __name__ == "__main__": + eliza_chat() diff --git a/nltk/chat/iesha.py b/nltk/chat/iesha.py new file mode 100644 index 0000000..b49e25f --- /dev/null +++ b/nltk/chat/iesha.py @@ -0,0 +1,160 @@ +# Natural Language Toolkit: Teen Chatbot +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Selina Dennis +# URL: +# For license information, see LICENSE.TXT + +""" +This chatbot is a tongue-in-cheek take on the average teen +anime junky that frequents YahooMessenger or MSNM. +All spelling mistakes and flawed grammar are intentional. +""" + +from nltk.chat.util import Chat + +reflections = { + "am": "r", + "was": "were", + "i": "u", + "i'd": "u'd", + "i've": "u'v", + "ive": "u'v", + "i'll": "u'll", + "my": "ur", + "are": "am", + "you're": "im", + "you've": "ive", + "you'll": "i'll", + "your": "my", + "yours": "mine", + "you": "me", + "u": "me", + "ur": "my", + "urs": "mine", + "me": "u", +} + +# Note: %1/2/etc are used without spaces prior as the chat bot seems +# to add a superfluous space when matching. + +pairs = ( + ( + r"I\'m (.*)", + ( + "ur%1?? that's so cool! kekekekeke ^_^ tell me more!", + "ur%1? neat!! kekeke >_<", + ), + ), + ( + r"(.*) don\'t you (.*)", + ( + r"u think I can%2??! really?? kekeke \<_\<", + "what do u mean%2??!", + "i could if i wanted, don't you think!! kekeke", + ), + ), + (r"ye[as] [iI] (.*)", ("u%1? cool!! how?", "how come u%1??", "u%1? so do i!!")), + ( + r"do (you|u) (.*)\??", + ("do i%2? only on tuesdays! kekeke *_*", "i dunno! do u%2??"), + ), + ( + r"(.*)\?", + ( + "man u ask lots of questions!", + "booooring! how old r u??", + "boooooring!! ur not very fun", + ), + ), + ( + r"(cos|because) (.*)", + ("hee! i don't believe u! >_<", "nuh-uh! >_<", "ooooh i agree!"), + ), + ( + r"why can\'t [iI] (.*)", + ( + "i dunno! y u askin me for!", + "try harder, silly! hee! ^_^", + "i dunno! but when i can't%1 i jump up and down!", + ), + ), + ( + r"I can\'t (.*)", + ( + "u can't what??! >_<", + "that's ok! i can't%1 either! kekekekeke ^_^", + "try harder, silly! hee! ^&^", + ), + ), + ( + r"(.*) (like|love|watch) anime", + ( + "omg i love anime!! do u like sailor moon??! ^&^", + "anime yay! anime rocks sooooo much!", + "oooh anime! i love anime more than anything!", + "anime is the bestest evar! evangelion is the best!", + "hee anime is the best! do you have ur fav??", + ), + ), + ( + r"I (like|love|watch|play) (.*)", + ("yay! %2 rocks!", "yay! %2 is neat!", "cool! do u like other stuff?? ^_^"), + ), + ( + r"anime sucks|(.*) (hate|detest) anime", + ( + "ur a liar! i'm not gonna talk to u nemore if u h8 anime *;*", + "no way! anime is the best ever!", + "nuh-uh, anime is the best!", + ), + ), + ( + r"(are|r) (you|u) (.*)", + ("am i%1??! how come u ask that!", "maybe! y shud i tell u?? kekeke >_>"), + ), + ( + r"what (.*)", + ("hee u think im gonna tell u? .v.", "booooooooring! ask me somethin else!"), + ), + (r"how (.*)", ("not tellin!! kekekekekeke ^_^",)), + (r"(hi|hello|hey) (.*)", ("hi!!! how r u!!",)), + ( + r"quit", + ( + "mom says i have to go eat dinner now :,( bye!!", + "awww u have to go?? see u next time!!", + "how to see u again soon! ^_^", + ), + ), + ( + r"(.*)", + ( + "ur funny! kekeke", + "boooooring! talk about something else! tell me wat u like!", + "do u like anime??", + "do u watch anime? i like sailor moon! ^_^", + "i wish i was a kitty!! kekekeke ^_^", + ), + ), +) + +iesha_chatbot = Chat(pairs, reflections) + + +def iesha_chat(): + print("Iesha the TeenBoT\n---------") + print("Talk to the program by typing in plain English, using normal upper-") + print('and lower-case letters and punctuation. Enter "quit" when done.') + print("=" * 72) + print("hi!! i'm iesha! who r u??!") + + iesha_chatbot.converse() + + +def demo(): + iesha_chat() + + +if __name__ == "__main__": + demo() diff --git a/nltk/chat/rude.py b/nltk/chat/rude.py new file mode 100644 index 0000000..415a4d2 --- /dev/null +++ b/nltk/chat/rude.py @@ -0,0 +1,125 @@ +# Natural Language Toolkit: Rude Chatbot +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Peter Spiller +# URL: +# For license information, see LICENSE.TXT + +from nltk.chat.util import Chat, reflections + +pairs = ( + ( + r"We (.*)", + ( + "What do you mean, 'we'?", + "Don't include me in that!", + "I wouldn't be so sure about that.", + ), + ), + ( + r"You should (.*)", + ("Don't tell me what to do, buddy.", "Really? I should, should I?"), + ), + ( + r"You\'re(.*)", + ( + "More like YOU'RE %1!", + "Hah! Look who's talking.", + "Come over here and tell me I'm %1.", + ), + ), + ( + r"You are(.*)", + ( + "More like YOU'RE %1!", + "Hah! Look who's talking.", + "Come over here and tell me I'm %1.", + ), + ), + ( + r"I can\'t(.*)", + ( + "You do sound like the type who can't %1.", + "Hear that splashing sound? That's my heart bleeding for you.", + "Tell somebody who might actually care.", + ), + ), + ( + r"I think (.*)", + ( + "I wouldn't think too hard if I were you.", + "You actually think? I'd never have guessed...", + ), + ), + ( + r"I (.*)", + ( + "I'm getting a bit tired of hearing about you.", + "How about we talk about me instead?", + "Me, me, me... Frankly, I don't care.", + ), + ), + ( + r"How (.*)", + ( + "How do you think?", + "Take a wild guess.", + "I'm not even going to dignify that with an answer.", + ), + ), + (r"What (.*)", ("Do I look like an encyclopedia?", "Figure it out yourself.")), + ( + r"Why (.*)", + ( + "Why not?", + "That's so obvious I thought even you'd have already figured it out.", + ), + ), + ( + r"(.*)shut up(.*)", + ( + "Make me.", + "Getting angry at a feeble NLP assignment? Somebody's losing it.", + "Say that again, I dare you.", + ), + ), + ( + r"Shut up(.*)", + ( + "Make me.", + "Getting angry at a feeble NLP assignment? Somebody's losing it.", + "Say that again, I dare you.", + ), + ), + ( + r"Hello(.*)", + ("Oh good, somebody else to talk to. Joy.", "'Hello'? How original..."), + ), + ( + r"(.*)", + ( + "I'm getting bored here. Become more interesting.", + "Either become more thrilling or get lost, buddy.", + "Change the subject before I die of fatal boredom.", + ), + ), +) + +rude_chatbot = Chat(pairs, reflections) + + +def rude_chat(): + print("Talk to the program by typing in plain English, using normal upper-") + print('and lower-case letters and punctuation. Enter "quit" when done.') + print("=" * 72) + print("I suppose I should say hello.") + + rude_chatbot.converse() + + +def demo(): + rude_chat() + + +if __name__ == "__main__": + demo() diff --git a/nltk/chat/suntsu.py b/nltk/chat/suntsu.py new file mode 100644 index 0000000..6cebdc6 --- /dev/null +++ b/nltk/chat/suntsu.py @@ -0,0 +1,140 @@ +# Natural Language Toolkit: Sun Tsu-Bot +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Sam Huston 2007 +# URL: +# For license information, see LICENSE.TXT + +""" +Tsu bot responds to all queries with a Sun Tsu sayings + +Quoted from Sun Tsu's The Art of War +Translated by LIONEL GILES, M.A. 1910 +Hosted by the Gutenberg Project +https://www.gutenberg.org/ +""" + +from nltk.chat.util import Chat, reflections + +pairs = ( + (r"quit", ("Good-bye.", "Plan well", "May victory be your future")), + ( + r"[^\?]*\?", + ( + "Please consider whether you can answer your own question.", + "Ask me no questions!", + ), + ), + ( + r"[0-9]+(.*)", + ( + "It is the rule in war, if our forces are ten to the enemy's one, to surround him; if five to one, to attack him; if twice as numerous, to divide our army into two.", + "There are five essentials for victory", + ), + ), + ( + r"[A-Ca-c](.*)", + ( + "The art of war is of vital importance to the State.", + "All warfare is based on deception.", + "If your opponent is secure at all points, be prepared for him. If he is in superior strength, evade him.", + "If the campaign is protracted, the resources of the State will not be equal to the strain.", + "Attack him where he is unprepared, appear where you are not expected.", + "There is no instance of a country having benefited from prolonged warfare.", + ), + ), + ( + r"[D-Fd-f](.*)", + ( + "The skillful soldier does not raise a second levy, neither are his supply-wagons loaded more than twice.", + "Bring war material with you from home, but forage on the enemy.", + "In war, then, let your great object be victory, not lengthy campaigns.", + "To fight and conquer in all your battles is not supreme excellence; supreme excellence consists in breaking the enemy's resistance without fighting.", + ), + ), + ( + r"[G-Ig-i](.*)", + ( + "Heaven signifies night and day, cold and heat, times and seasons.", + "It is the rule in war, if our forces are ten to the enemy's one, to surround him; if five to one, to attack him; if twice as numerous, to divide our army into two.", + "The good fighters of old first put themselves beyond the possibility of defeat, and then waited for an opportunity of defeating the enemy.", + "One may know how to conquer without being able to do it.", + ), + ), + ( + r"[J-Lj-l](.*)", + ( + "There are three ways in which a ruler can bring misfortune upon his army.", + "By commanding the army to advance or to retreat, being ignorant of the fact that it cannot obey. This is called hobbling the army.", + "By attempting to govern an army in the same way as he administers a kingdom, being ignorant of the conditions which obtain in an army. This causes restlessness in the soldier's minds.", + "By employing the officers of his army without discrimination, through ignorance of the military principle of adaptation to circumstances. This shakes the confidence of the soldiers.", + "There are five essentials for victory", + "He will win who knows when to fight and when not to fight.", + "He will win who knows how to handle both superior and inferior forces.", + "He will win whose army is animated by the same spirit throughout all its ranks.", + "He will win who, prepared himself, waits to take the enemy unprepared.", + "He will win who has military capacity and is not interfered with by the sovereign.", + ), + ), + ( + r"[M-Om-o](.*)", + ( + "If you know the enemy and know yourself, you need not fear the result of a hundred battles.", + "If you know yourself but not the enemy, for every victory gained you will also suffer a defeat.", + "If you know neither the enemy nor yourself, you will succumb in every battle.", + "The control of a large force is the same principle as the control of a few men: it is merely a question of dividing up their numbers.", + ), + ), + ( + r"[P-Rp-r](.*)", + ( + "Security against defeat implies defensive tactics; ability to defeat the enemy means taking the offensive.", + "Standing on the defensive indicates insufficient strength; attacking, a superabundance of strength.", + "He wins his battles by making no mistakes. Making no mistakes is what establishes the certainty of victory, for it means conquering an enemy that is already defeated.", + "A victorious army opposed to a routed one, is as a pound's weight placed in the scale against a single grain.", + "The onrush of a conquering force is like the bursting of pent-up waters into a chasm a thousand fathoms deep.", + ), + ), + ( + r"[S-Us-u](.*)", + ( + "What the ancients called a clever fighter is one who not only wins, but excels in winning with ease.", + "Hence his victories bring him neither reputation for wisdom nor credit for courage.", + "Hence the skillful fighter puts himself into a position which makes defeat impossible, and does not miss the moment for defeating the enemy.", + "In war the victorious strategist only seeks battle after the victory has been won, whereas he who is destined to defeat first fights and afterwards looks for victory.", + "There are not more than five musical notes, yet the combinations of these five give rise to more melodies than can ever be heard.", + "Appear at points which the enemy must hasten to defend; march swiftly to places where you are not expected.", + ), + ), + ( + r"[V-Zv-z](.*)", + ( + "It is a matter of life and death, a road either to safety or to ruin.", + "Hold out baits to entice the enemy. Feign disorder, and crush him.", + "All men can see the tactics whereby I conquer, but what none can see is the strategy out of which victory is evolved.", + "Do not repeat the tactics which have gained you one victory, but let your methods be regulated by the infinite variety of circumstances.", + "So in war, the way is to avoid what is strong and to strike at what is weak.", + "Just as water retains no constant shape, so in warfare there are no constant conditions.", + ), + ), + (r"(.*)", ("Your statement insults me.", "")), +) + +suntsu_chatbot = Chat(pairs, reflections) + + +def suntsu_chat(): + print("Talk to the program by typing in plain English, using normal upper-") + print('and lower-case letters and punctuation. Enter "quit" when done.') + print("=" * 72) + print("You seek enlightenment?") + + suntsu_chatbot.converse() + + +def demo(): + suntsu_chat() + + +if __name__ == "__main__": + demo() diff --git a/nltk/chat/util.py b/nltk/chat/util.py new file mode 100644 index 0000000..8f3e9f8 --- /dev/null +++ b/nltk/chat/util.py @@ -0,0 +1,124 @@ +# Natural Language Toolkit: Chatbot Utilities +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +# Based on an Eliza implementation by Joe Strout , +# Jeff Epler and Jez Higgins . + +import random +import re + +reflections = { + "i am": "you are", + "i was": "you were", + "i": "you", + "i'm": "you are", + "i'd": "you would", + "i've": "you have", + "i'll": "you will", + "my": "your", + "you are": "I am", + "you were": "I was", + "you've": "I have", + "you'll": "I will", + "your": "my", + "yours": "mine", + "you": "me", + "me": "you", +} + + +class Chat: + def __init__(self, pairs, reflections={}): + """ + Initialize the chatbot. Pairs is a list of patterns and responses. Each + pattern is a regular expression matching the user's statement or question, + e.g. r'I like (.*)'. For each such pattern a list of possible responses + is given, e.g. ['Why do you like %1', 'Did you ever dislike %1']. Material + which is matched by parenthesized sections of the patterns (e.g. .*) is mapped to + the numbered positions in the responses, e.g. %1. + + :type pairs: list of tuple + :param pairs: The patterns and responses + :type reflections: dict + :param reflections: A mapping between first and second person expressions + :rtype: None + """ + + self._pairs = [(re.compile(x, re.IGNORECASE), y) for (x, y) in pairs] + self._reflections = reflections + self._regex = self._compile_reflections() + + def _compile_reflections(self): + sorted_refl = sorted(self._reflections, key=len, reverse=True) + return re.compile( + r"\b({})\b".format("|".join(map(re.escape, sorted_refl))), re.IGNORECASE + ) + + def _substitute(self, str): + """ + Substitute words in the string, according to the specified reflections, + e.g. "I'm" -> "you are" + + :type str: str + :param str: The string to be mapped + :rtype: str + """ + + return self._regex.sub( + lambda mo: self._reflections[mo.string[mo.start() : mo.end()]], str.lower() + ) + + def _wildcards(self, response, match): + pos = response.find("%") + while pos >= 0: + num = int(response[pos + 1 : pos + 2]) + response = ( + response[:pos] + + self._substitute(match.group(num)) + + response[pos + 2 :] + ) + pos = response.find("%") + return response + + def respond(self, str): + """ + Generate a response to the user input. + + :type str: str + :param str: The string to be mapped + :rtype: str + """ + + # check each pattern + for pattern, response in self._pairs: + match = pattern.match(str) + + # did the pattern match? + if match: + resp = random.choice(response) # pick a random response + resp = self._wildcards(resp, match) # process wildcards + + # fix munged punctuation at the end + if resp[-2:] == "?.": + resp = resp[:-2] + "." + if resp[-2:] == "??": + resp = resp[:-2] + "?" + return resp + + # Hold a conversation with a chatbot + def converse(self, quit="quit"): + user_input = "" + while user_input != quit: + user_input = quit + try: + user_input = input(">") + except EOFError: + print(user_input) + if user_input: + while user_input[-1] in "!.": + user_input = user_input[:-1] + print(self.respond(user_input)) diff --git a/nltk/chat/zen.py b/nltk/chat/zen.py new file mode 100644 index 0000000..858553e --- /dev/null +++ b/nltk/chat/zen.py @@ -0,0 +1,329 @@ +# Natural Language Toolkit: Zen Chatbot +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Amy Holland +# URL: +# For license information, see LICENSE.TXT + +""" +Zen Chatbot talks in gems of Zen wisdom. + +This is a sample conversation with Zen Chatbot: +ZC: Welcome, my child. +me: Good afternoon. +ZC: Ask the question you have come to ask. +me: How can I achieve enlightenment? +ZC: How do you suppose? +me: Through meditation. +ZC: Form is emptiness, and emptiness form. +me: How can I empty my mind of worldly troubles? +ZC: Will an answer to that really help in your search for enlightenment? +me: Yes. +ZC: It is better to be right than to be certain. +me: I seek truth and wisdom. +ZC: The search for truth is a long journey. +me: Are you sure? +ZC: Maybe sure, maybe not sure. + + +The chatbot structure is based on that of chat.eliza. Thus, it uses +a translation table to convert from question to response +i.e. "I am" --> "you are" + +Of course, since Zen Chatbot does not understand the meaning of any words, +responses are very limited. Zen Chatbot will usually answer very vaguely, or +respond to a question by asking a different question, in much the same way +as Eliza. +""" + +from nltk.chat.util import Chat, reflections + +# responses are matched top to bottom, so non-specific matches occur later +# for each match, a list of possible responses is provided +responses = ( + # Zen Chatbot opens with the line "Welcome, my child." The usual + # response will be a greeting problem: 'good' matches "good morning", + # "good day" etc, but also "good grief!" and other sentences starting + # with the word 'good' that may not be a greeting + ( + r"(hello(.*))|(good [a-zA-Z]+)", + ( + "The path to enlightenment is often difficult to see.", + "Greetings. I sense your mind is troubled. Tell me of your troubles.", + "Ask the question you have come to ask.", + "Hello. Do you seek englightenment?", + ), + ), + # "I need" and "I want" can be followed by a thing (eg 'help') + # or an action (eg 'to see you') + # + # This is a problem with this style of response - + # person: "I need you" + # chatbot: "me can be achieved by hard work and dedication of the mind" + # i.e. 'you' is not really a thing that can be mapped this way, so this + # interpretation only makes sense for some inputs + # + ( + r"i need (.*)", + ( + "%1 can be achieved by hard work and dedication of the mind.", + "%1 is not a need, but a desire of the mind. Clear your mind of such concerns.", + "Focus your mind on%1, and you will find what you need.", + ), + ), + ( + r"i want (.*)", + ( + "Desires of the heart will distract you from the path to enlightenment.", + "Will%1 help you attain enlightenment?", + "Is%1 a desire of the mind, or of the heart?", + ), + ), + # why questions are separated into three types: + # "why..I" e.g. "why am I here?" "Why do I like cake?" + # "why..you" e.g. "why are you here?" "Why won't you tell me?" + # "why..." e.g. "Why is the sky blue?" + # problems: + # person: "Why can't you tell me?" + # chatbot: "Are you sure I tell you?" + # - this style works for positives (e.g. "why do you like cake?") + # but does not work for negatives (e.g. "why don't you like cake?") + (r"why (.*) i (.*)\?", ("You%1%2?", "Perhaps you only think you%1%2")), + (r"why (.*) you(.*)\?", ("Why%1 you%2?", "%2 I%1", "Are you sure I%2?")), + (r"why (.*)\?", ("I cannot tell you why%1.", "Why do you think %1?")), + # e.g. "are you listening?", "are you a duck" + ( + r"are you (.*)\?", + ("Maybe%1, maybe not%1.", "Whether I am%1 or not is God's business."), + ), + # e.g. "am I a duck?", "am I going to die?" + ( + r"am i (.*)\?", + ("Perhaps%1, perhaps not%1.", "Whether you are%1 or not is not for me to say."), + ), + # what questions, e.g. "what time is it?" + # problems: + # person: "What do you want?" + # chatbot: "Seek truth, not what do me want." + (r"what (.*)\?", ("Seek truth, not what%1.", "What%1 should not concern you.")), + # how questions, e.g. "how do you do?" + ( + r"how (.*)\?", + ( + "How do you suppose?", + "Will an answer to that really help in your search for enlightenment?", + "Ask yourself not how, but why.", + ), + ), + # can questions, e.g. "can you run?", "can you come over here please?" + ( + r"can you (.*)\?", + ( + "I probably can, but I may not.", + "Maybe I can%1, and maybe I cannot.", + "I can do all, and I can do nothing.", + ), + ), + # can questions, e.g. "can I have some cake?", "can I know truth?" + ( + r"can i (.*)\?", + ( + "You can%1 if you believe you can%1, and have a pure spirit.", + "Seek truth and you will know if you can%1.", + ), + ), + # e.g. "It is raining" - implies the speaker is certain of a fact + ( + r"it is (.*)", + ( + "How can you be certain that%1, when you do not even know yourself?", + "Whether it is%1 or not does not change the way the world is.", + ), + ), + # e.g. "is there a doctor in the house?" + ( + r"is there (.*)\?", + ("There is%1 if you believe there is.", "It is possible that there is%1."), + ), + # e.g. "is it possible?", "is this true?" + (r"is(.*)\?", ("%1 is not relevant.", "Does this matter?")), + # non-specific question + ( + r"(.*)\?", + ( + "Do you think %1?", + "You seek the truth. Does the truth seek you?", + "If you intentionally pursue the answers to your questions, the answers become hard to see.", + "The answer to your question cannot be told. It must be experienced.", + ), + ), + # expression of hate of form "I hate you" or "Kelly hates cheese" + ( + r"(.*) (hate[s]?)|(dislike[s]?)|(don\'t like)(.*)", + ( + "Perhaps it is not about hating %2, but about hate from within.", + "Weeds only grow when we dislike them", + "Hate is a very strong emotion.", + ), + ), + # statement containing the word 'truth' + ( + r"(.*) truth(.*)", + ( + "Seek truth, and truth will seek you.", + "Remember, it is not the spoon which bends - only yourself.", + "The search for truth is a long journey.", + ), + ), + # desire to do an action + # e.g. "I want to go shopping" + ( + r"i want to (.*)", + ("You may %1 if your heart truly desires to.", "You may have to %1."), + ), + # desire for an object + # e.g. "I want a pony" + ( + r"i want (.*)", + ( + "Does your heart truly desire %1?", + "Is this a desire of the heart, or of the mind?", + ), + ), + # e.g. "I can't wait" or "I can't do this" + ( + r"i can\'t (.*)", + ( + "What we can and can't do is a limitation of the mind.", + "There are limitations of the body, and limitations of the mind.", + "Have you tried to%1 with a clear mind?", + ), + ), + # "I think.." indicates uncertainty. e.g. "I think so." + # problem: exceptions... + # e.g. "I think, therefore I am" + ( + r"i think (.*)", + ( + "Uncertainty in an uncertain world.", + "Indeed, how can we be certain of anything in such uncertain times.", + "Are you not, in fact, certain that%1?", + ), + ), + # "I feel...emotions/sick/light-headed..." + ( + r"i feel (.*)", + ( + "Your body and your emotions are both symptoms of your mind." + "What do you believe is the root of such feelings?", + "Feeling%1 can be a sign of your state-of-mind.", + ), + ), + # exclaimation mark indicating emotion + # e.g. "Wow!" or "No!" + ( + r"(.*)!", + ( + "I sense that you are feeling emotional today.", + "You need to calm your emotions.", + ), + ), + # because [statement] + # e.g. "because I said so" + ( + r"because (.*)", + ( + "Does knowning the reasons behind things help you to understand" + " the things themselves?", + "If%1, what else must be true?", + ), + ), + # yes or no - raise an issue of certainty/correctness + ( + r"(yes)|(no)", + ( + "Is there certainty in an uncertain world?", + "It is better to be right than to be certain.", + ), + ), + # sentence containing word 'love' + ( + r"(.*)love(.*)", + ( + "Think of the trees: they let the birds perch and fly with no intention to call them when they come, and no longing for their return when they fly away. Let your heart be like the trees.", + "Free love!", + ), + ), + # sentence containing word 'understand' - r + ( + r"(.*)understand(.*)", + ( + "If you understand, things are just as they are;" + " if you do not understand, things are just as they are.", + "Imagination is more important than knowledge.", + ), + ), + # 'I', 'me', 'my' - person is talking about themself. + # this breaks down when words contain these - eg 'Thyme', 'Irish' + ( + r"(.*)(me )|( me)|(my)|(mine)|(i)(.*)", + ( + "'I', 'me', 'my'... these are selfish expressions.", + "Have you ever considered that you might be a selfish person?", + "Try to consider others, not just yourself.", + "Think not just of yourself, but of others.", + ), + ), + # 'you' starting a sentence + # e.g. "you stink!" + ( + r"you (.*)", + ("My path is not of concern to you.", "I am but one, and you but one more."), + ), + # say goodbye with some extra Zen wisdom. + ( + r"exit", + ( + "Farewell. The obstacle is the path.", + "Farewell. Life is a journey, not a destination.", + "Good bye. We are cups, constantly and quietly being filled." + "\nThe trick is knowning how to tip ourselves over and let the beautiful stuff out.", + ), + ), + # fall through case - + # when stumped, respond with generic zen wisdom + # + ( + r"(.*)", + ( + "When you're enlightened, every word is wisdom.", + "Random talk is useless.", + "The reverse side also has a reverse side.", + "Form is emptiness, and emptiness is form.", + "I pour out a cup of water. Is the cup empty?", + ), + ), +) + +zen_chatbot = Chat(responses, reflections) + + +def zen_chat(): + print("*" * 75) + print("Zen Chatbot!".center(75)) + print("*" * 75) + print('"Look beyond mere words and letters - look into your mind"'.center(75)) + print("* Talk your way to truth with Zen Chatbot.") + print("* Type 'quit' when you have had enough.") + print("*" * 75) + print("Welcome, my child.") + + zen_chatbot.converse() + + +def demo(): + zen_chat() + + +if __name__ == "__main__": + demo() diff --git a/nltk/chunk/__init__.py b/nltk/chunk/__init__.py new file mode 100644 index 0000000..f457f5d --- /dev/null +++ b/nltk/chunk/__init__.py @@ -0,0 +1,205 @@ +# Natural Language Toolkit: Chunkers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT +# + +""" +Classes and interfaces for identifying non-overlapping linguistic +groups (such as base noun phrases) in unrestricted text. This task is +called "chunk parsing" or "chunking", and the identified groups are +called "chunks". The chunked text is represented using a shallow +tree called a "chunk structure." A chunk structure is a tree +containing tokens and chunks, where each chunk is a subtree containing +only tokens. For example, the chunk structure for base noun phrase +chunks in the sentence "I saw the big dog on the hill" is:: + + (SENTENCE: + (NP: ) + + (NP: ) + + (NP: )) + +To convert a chunk structure back to a list of tokens, simply use the +chunk structure's ``leaves()`` method. + +This module defines ``ChunkParserI``, a standard interface for +chunking texts; and ``RegexpChunkParser``, a regular-expression based +implementation of that interface. It also defines ``ChunkScore``, a +utility class for scoring chunk parsers. + +RegexpChunkParser +================= + +``RegexpChunkParser`` is an implementation of the chunk parser interface +that uses regular-expressions over tags to chunk a text. Its +``parse()`` method first constructs a ``ChunkString``, which encodes a +particular chunking of the input text. Initially, nothing is +chunked. ``parse.RegexpChunkParser`` then applies a sequence of +``RegexpChunkRule`` rules to the ``ChunkString``, each of which modifies +the chunking that it encodes. Finally, the ``ChunkString`` is +transformed back into a chunk structure, which is returned. + +``RegexpChunkParser`` can only be used to chunk a single kind of phrase. +For example, you can use an ``RegexpChunkParser`` to chunk the noun +phrases in a text, or the verb phrases in a text; but you can not +use it to simultaneously chunk both noun phrases and verb phrases in +the same text. (This is a limitation of ``RegexpChunkParser``, not of +chunk parsers in general.) + +RegexpChunkRules +---------------- + +A ``RegexpChunkRule`` is a transformational rule that updates the +chunking of a text by modifying its ``ChunkString``. Each +``RegexpChunkRule`` defines the ``apply()`` method, which modifies +the chunking encoded by a ``ChunkString``. The +``RegexpChunkRule`` class itself can be used to implement any +transformational rule based on regular expressions. There are +also a number of subclasses, which can be used to implement +simpler types of rules: + + - ``ChunkRule`` chunks anything that matches a given regular + expression. + - ``StripRule`` strips anything that matches a given regular + expression. + - ``UnChunkRule`` will un-chunk any chunk that matches a given + regular expression. + - ``MergeRule`` can be used to merge two contiguous chunks. + - ``SplitRule`` can be used to split a single chunk into two + smaller chunks. + - ``ExpandLeftRule`` will expand a chunk to incorporate new + unchunked material on the left. + - ``ExpandRightRule`` will expand a chunk to incorporate new + unchunked material on the right. + +Tag Patterns +~~~~~~~~~~~~ + +A ``RegexpChunkRule`` uses a modified version of regular +expression patterns, called "tag patterns". Tag patterns are +used to match sequences of tags. Examples of tag patterns are:: + + r'(
    ||)+' + r'+' + r'' + +The differences between regular expression patterns and tag +patterns are: + + - In tag patterns, ``'<'`` and ``'>'`` act as parentheses; so + ``'+'`` matches one or more repetitions of ``''``, not + ``''``. + - Whitespace in tag patterns is ignored. So + ``'
    | '`` is equivalent to ``'
    |'`` + - In tag patterns, ``'.'`` is equivalent to ``'[^{}<>]'``; so + ``''`` matches any single tag starting with ``'NN'``. + +The function ``tag_pattern2re_pattern`` can be used to transform +a tag pattern to an equivalent regular expression pattern. + +Efficiency +---------- + +Preliminary tests indicate that ``RegexpChunkParser`` can chunk at a +rate of about 300 tokens/second, with a moderately complex rule set. + +There may be problems if ``RegexpChunkParser`` is used with more than +5,000 tokens at a time. In particular, evaluation of some regular +expressions may cause the Python regular expression engine to +exceed its maximum recursion depth. We have attempted to minimize +these problems, but it is impossible to avoid them completely. We +therefore recommend that you apply the chunk parser to a single +sentence at a time. + +Emacs Tip +--------- + +If you evaluate the following elisp expression in emacs, it will +colorize a ``ChunkString`` when you use an interactive python shell +with emacs or xemacs ("C-c !"):: + + (let () + (defconst comint-mode-font-lock-keywords + '(("<[^>]+>" 0 'font-lock-reference-face) + ("[{}]" 0 'font-lock-function-name-face))) + (add-hook 'comint-mode-hook (lambda () (turn-on-font-lock)))) + +You can evaluate this code by copying it to a temporary buffer, +placing the cursor after the last close parenthesis, and typing +"``C-x C-e``". You should evaluate it before running the interactive +session. The change will last until you close emacs. + +Unresolved Issues +----------------- + +If we use the ``re`` module for regular expressions, Python's +regular expression engine generates "maximum recursion depth +exceeded" errors when processing very large texts, even for +regular expressions that should not require any recursion. We +therefore use the ``pre`` module instead. But note that ``pre`` +does not include Unicode support, so this module will not work +with unicode strings. Note also that ``pre`` regular expressions +are not quite as advanced as ``re`` ones (e.g., no leftward +zero-length assertions). + +:type CHUNK_TAG_PATTERN: regexp +:var CHUNK_TAG_PATTERN: A regular expression to test whether a tag + pattern is valid. +""" + +from nltk.chunk.api import ChunkParserI +from nltk.chunk.named_entity import Maxent_NE_Chunker +from nltk.chunk.regexp import RegexpChunkParser, RegexpParser +from nltk.chunk.util import ( + ChunkScore, + accuracy, + conllstr2tree, + conlltags2tree, + ieerstr2tree, + tagstr2tree, + tree2conllstr, + tree2conlltags, +) + + +def ne_chunker(fmt="multiclass"): + """ + Load NLTK's currently recommended named entity chunker. + """ + return Maxent_NE_Chunker(fmt) + + +def ne_chunk(tagged_tokens, binary=False): + """ + Use NLTK's currently recommended named entity chunker to + chunk the given list of tagged tokens. + + >>> from nltk.chunk import ne_chunk + >>> from nltk.corpus import treebank + >>> from pprint import pprint + >>> pprint(ne_chunk(treebank.tagged_sents()[2][8:14])) # doctest: +NORMALIZE_WHITESPACE + Tree('S', [('chairman', 'NN'), ('of', 'IN'), Tree('ORGANIZATION', [('Consolidated', 'NNP'), ('Gold', 'NNP'), ('Fields', 'NNP')]), ('PLC', 'NNP')]) + + """ + if binary: + chunker = ne_chunker(fmt="binary") + else: + chunker = ne_chunker() + return chunker.parse(tagged_tokens) + + +def ne_chunk_sents(tagged_sentences, binary=False): + """ + Use NLTK's currently recommended named entity chunker to chunk the + given list of tagged sentences, each consisting of a list of tagged tokens. + """ + if binary: + chunker = ne_chunker(fmt="binary") + else: + chunker = ne_chunker() + return chunker.parse_sents(tagged_sentences) diff --git a/nltk/chunk/api.py b/nltk/chunk/api.py new file mode 100644 index 0000000..86d79a9 --- /dev/null +++ b/nltk/chunk/api.py @@ -0,0 +1,56 @@ +# Natural Language Toolkit: Chunk parsing API +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# URL: +# For license information, see LICENSE.TXT + +##////////////////////////////////////////////////////// +## Chunk Parser Interface +##////////////////////////////////////////////////////// + +from nltk.chunk.util import ChunkScore +from nltk.internals import deprecated +from nltk.parse import ParserI + + +class ChunkParserI(ParserI): + """ + A processing interface for identifying non-overlapping groups in + unrestricted text. Typically, chunk parsers are used to find base + syntactic constituents, such as base noun phrases. Unlike + ``ParserI``, ``ChunkParserI`` guarantees that the ``parse()`` method + will always generate a parse. + """ + + def parse(self, tokens): + """ + Return the best chunk structure for the given tokens + and return a tree. + + :param tokens: The list of (word, tag) tokens to be chunked. + :type tokens: list(tuple) + :rtype: Tree + """ + raise NotImplementedError() + + @deprecated("Use accuracy(gold) instead.") + def evaluate(self, gold): + return self.accuracy(gold) + + def accuracy(self, gold): + """ + Score the accuracy of the chunker against the gold standard. + Remove the chunking the gold standard text, rechunk it using + the chunker, and return a ``ChunkScore`` object + reflecting the performance of this chunk parser. + + :type gold: list(Tree) + :param gold: The list of chunked sentences to score the chunker on. + :rtype: ChunkScore + """ + chunkscore = ChunkScore() + for correct in gold: + chunkscore.score(correct, self.parse(correct.leaves())) + return chunkscore diff --git a/nltk/chunk/named_entity.py b/nltk/chunk/named_entity.py new file mode 100644 index 0000000..f66884e --- /dev/null +++ b/nltk/chunk/named_entity.py @@ -0,0 +1,406 @@ +# Natural Language Toolkit: Chunk parsing API +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Eric Kafe (tab-format models) +# URL: +# For license information, see LICENSE.TXT + +""" +Named entity chunker +""" + +import os +import re +from xml.etree import ElementTree as ET + +from nltk.tag import ClassifierBasedTagger, pos_tag + +try: + from nltk.classify import MaxentClassifier +except ImportError: + pass + +from nltk.chunk.api import ChunkParserI +from nltk.chunk.util import ChunkScore +from nltk.data import find +from nltk.tokenize import word_tokenize +from nltk.tree import Tree + + +class NEChunkParserTagger(ClassifierBasedTagger): + """ + The IOB tagger used by the chunk parser. + """ + + def __init__(self, train=None, classifier=None): + ClassifierBasedTagger.__init__( + self, + train=train, + classifier_builder=self._classifier_builder, + classifier=classifier, + ) + + def _classifier_builder(self, train): + return MaxentClassifier.train( + # "megam" cannot be the default algorithm since it requires compiling with ocaml + train, + algorithm="iis", + gaussian_prior_sigma=1, + trace=2, + ) + + def _english_wordlist(self): + try: + wl = self._en_wordlist + except AttributeError: + from nltk.corpus import words + + self._en_wordlist = set(words.words("en-basic")) + wl = self._en_wordlist + return wl + + def _feature_detector(self, tokens, index, history): + word = tokens[index][0] + pos = simplify_pos(tokens[index][1]) + if index == 0: + prevword = prevprevword = None + prevpos = prevprevpos = None + prevshape = prevtag = prevprevtag = None + elif index == 1: + prevword = tokens[index - 1][0].lower() + prevprevword = None + prevpos = simplify_pos(tokens[index - 1][1]) + prevprevpos = None + prevtag = history[index - 1][0] + prevshape = prevprevtag = None + else: + prevword = tokens[index - 1][0].lower() + prevprevword = tokens[index - 2][0].lower() + prevpos = simplify_pos(tokens[index - 1][1]) + prevprevpos = simplify_pos(tokens[index - 2][1]) + prevtag = history[index - 1] + prevprevtag = history[index - 2] + prevshape = shape(prevword) + if index == len(tokens) - 1: + nextword = nextnextword = None + nextpos = nextnextpos = None + elif index == len(tokens) - 2: + nextword = tokens[index + 1][0].lower() + nextpos = tokens[index + 1][1].lower() + nextnextword = None + nextnextpos = None + else: + nextword = tokens[index + 1][0].lower() + nextpos = tokens[index + 1][1].lower() + nextnextword = tokens[index + 2][0].lower() + nextnextpos = tokens[index + 2][1].lower() + + # 89.6 + features = { + "bias": True, + "shape": shape(word), + "wordlen": len(word), + "prefix3": word[:3].lower(), + "suffix3": word[-3:].lower(), + "pos": pos, + "word": word, + "en-wordlist": (word in self._english_wordlist()), + "prevtag": prevtag, + "prevpos": prevpos, + "nextpos": nextpos, + "prevword": prevword, + "nextword": nextword, + "word+nextpos": f"{word.lower()}+{nextpos}", + "pos+prevtag": f"{pos}+{prevtag}", + "shape+prevtag": f"{prevshape}+{prevtag}", + } + + return features + + +class NEChunkParser(ChunkParserI): + """ + Expected input: list of pos-tagged words + """ + + def __init__(self, train): + self._train(train) + + def parse(self, tokens): + """ + Each token should be a pos-tagged word + """ + tagged = self._tagger.tag(tokens) + tree = self._tagged_to_parse(tagged) + return tree + + def _train(self, corpus): + # Convert to tagged sequence + corpus = [self._parse_to_tagged(s) for s in corpus] + + self._tagger = NEChunkParserTagger(train=corpus) + + def _tagged_to_parse(self, tagged_tokens): + """ + Convert a list of tagged tokens to a chunk-parse tree. + """ + sent = Tree("S", []) + + for tok, tag in tagged_tokens: + if tag == "O": + sent.append(tok) + elif tag.startswith("B-"): + sent.append(Tree(tag[2:], [tok])) + elif tag.startswith("I-"): + if sent and isinstance(sent[-1], Tree) and sent[-1].label() == tag[2:]: + sent[-1].append(tok) + else: + sent.append(Tree(tag[2:], [tok])) + return sent + + @staticmethod + def _parse_to_tagged(sent): + """ + Convert a chunk-parse tree to a list of tagged tokens. + """ + toks = [] + for child in sent: + if isinstance(child, Tree): + if len(child) == 0: + print("Warning -- empty chunk in sentence") + continue + toks.append((child[0], f"B-{child.label()}")) + for tok in child[1:]: + toks.append((tok, f"I-{child.label()}")) + else: + toks.append((child, "O")) + return toks + + +def shape(word): + if re.match(r"[0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+$", word, re.UNICODE): + return "number" + elif re.match(r"\W+$", word, re.UNICODE): + return "punct" + elif re.match(r"\w+$", word, re.UNICODE): + if word.istitle(): + return "upcase" + elif word.islower(): + return "downcase" + else: + return "mixedcase" + else: + return "other" + + +def simplify_pos(s): + if s.startswith("V"): + return "V" + else: + return s.split("-")[0] + + +def postag_tree(tree): + # Part-of-speech tagging. + words = tree.leaves() + tag_iter = (pos for (word, pos) in pos_tag(words)) + newtree = Tree("S", []) + for child in tree: + if isinstance(child, Tree): + newtree.append(Tree(child.label(), [])) + for subchild in child: + newtree[-1].append((subchild, next(tag_iter))) + else: + newtree.append((child, next(tag_iter))) + return newtree + + +def load_ace_data(roots, fmt="binary", skip_bnews=True): + for root in roots: + for root, dirs, files in os.walk(root): + if root.endswith("bnews") and skip_bnews: + continue + for f in files: + if f.endswith(".sgm"): + yield from load_ace_file(os.path.join(root, f), fmt) + + +def load_ace_file(textfile, fmt): + print(f" - {os.path.split(textfile)[1]}") + annfile = textfile + ".tmx.rdc.xml" + + # Read the xml file, and get a list of entities + entities = [] + with open(annfile) as infile: + xml = ET.parse(infile).getroot() + for entity in xml.findall("document/entity"): + typ = entity.find("entity_type").text + for mention in entity.findall("entity_mention"): + if mention.get("TYPE") != "NAME": + continue # only NEs + s = int(mention.find("head/charseq/start").text) + e = int(mention.find("head/charseq/end").text) + 1 + entities.append((s, e, typ)) + + # Read the text file, and mark the entities. + with open(textfile) as infile: + text = infile.read() + + # Strip XML tags, since they don't count towards the indices + text = re.sub("<(?!/?TEXT)[^>]+>", "", text) + + # Blank out anything before/after + def subfunc(m): + return " " * (m.end() - m.start() - 6) + + text = re.sub(r"[\s\S]*", subfunc, text) + text = re.sub(r"[\s\S]*", "", text) + + # Simplify quotes + text = re.sub("``", ' "', text) + text = re.sub("''", '" ', text) + + entity_types = {typ for (s, e, typ) in entities} + + # Binary distinction (NE or not NE) + if fmt == "binary": + i = 0 + toks = Tree("S", []) + for s, e, typ in sorted(entities): + if s < i: + s = i # Overlapping! Deal with this better? + if e <= s: + continue + toks.extend(word_tokenize(text[i:s])) + toks.append(Tree("NE", text[s:e].split())) + i = e + toks.extend(word_tokenize(text[i:])) + yield toks + + # Multiclass distinction (NE type) + elif fmt == "multiclass": + i = 0 + toks = Tree("S", []) + for s, e, typ in sorted(entities): + if s < i: + s = i # Overlapping! Deal with this better? + if e <= s: + continue + toks.extend(word_tokenize(text[i:s])) + toks.append(Tree(typ, text[s:e].split())) + i = e + toks.extend(word_tokenize(text[i:])) + yield toks + + else: + raise ValueError("bad fmt value") + + +# This probably belongs in a more general-purpose location (as does +# the parse_to_tagged function). +def cmp_chunks(correct, guessed): + correct = NEChunkParser._parse_to_tagged(correct) + guessed = NEChunkParser._parse_to_tagged(guessed) + ellipsis = False + for (w, ct), (w, gt) in zip(correct, guessed): + if ct == gt == "O": + if not ellipsis: + print(f" {ct:15} {gt:15} {w}") + print(" {:15} {:15} {}".format("...", "...", "...")) + ellipsis = True + else: + ellipsis = False + print(f" {ct:15} {gt:15} {w}") + + +# ====================================================================================== + + +class Maxent_NE_Chunker(NEChunkParser): + """ + Expected input: list of pos-tagged words + """ + + def __init__(self, fmt="multiclass"): + + self._fmt = fmt + self._tab_dir = find(f"chunkers/maxent_ne_chunker_tab/english_ace_{fmt}/") + self.load_params() + + def load_params(self): + from nltk.classify.maxent import BinaryMaxentFeatureEncoding, load_maxent_params + + wgt, mpg, lab, aon = load_maxent_params(self._tab_dir) + mc = MaxentClassifier( + BinaryMaxentFeatureEncoding(lab, mpg, alwayson_features=aon), wgt + ) + self._tagger = NEChunkParserTagger(classifier=mc) + + def save_params(self): + from nltk.classify.maxent import save_maxent_params + + classif = self._tagger._classifier + ecg = classif._encoding + wgt = classif._weights + mpg = ecg._mapping + lab = ecg._labels + aon = ecg._alwayson + fmt = self._fmt + save_maxent_params(wgt, mpg, lab, aon, tab_dir=f"/tmp/english_ace_{fmt}/") + + +def build_model(fmt="multiclass"): + chunker = Maxent_NE_Chunker(fmt) + chunker.save_params() + return chunker + + +# ====================================================================================== + +""" +2004 update: pickles are not supported anymore. + +Deprecated: + +def build_model(fmt="binary"): + print("Loading training data...") + train_paths = [ + find("corpora/ace_data/ace.dev"), + find("corpora/ace_data/ace.heldout"), + find("corpora/ace_data/bbn.dev"), + find("corpora/ace_data/muc.dev"), + ] + train_trees = load_ace_data(train_paths, fmt) + train_data = [postag_tree(t) for t in train_trees] + print("Training...") + cp = NEChunkParser(train_data) + del train_data + + print("Loading eval data...") + eval_paths = [find("corpora/ace_data/ace.eval")] + eval_trees = load_ace_data(eval_paths, fmt) + eval_data = [postag_tree(t) for t in eval_trees] + + print("Evaluating...") + chunkscore = ChunkScore() + for i, correct in enumerate(eval_data): + guess = cp.parse(correct.leaves()) + chunkscore.score(correct, guess) + if i < 3: + cmp_chunks(correct, guess) + print(chunkscore) + + outfilename = f"/tmp/ne_chunker_{fmt}.pickle" + print(f"Saving chunker to {outfilename}...") + + with open(outfilename, "wb") as outfile: + pickle.dump(cp, outfile, -1) + + return cp +""" + +if __name__ == "__main__": + # Make sure that the object has the right class name: + build_model("binary") + build_model("multiclass") diff --git a/nltk/chunk/regexp.py b/nltk/chunk/regexp.py new file mode 100644 index 0000000..c045c8b --- /dev/null +++ b/nltk/chunk/regexp.py @@ -0,0 +1,1483 @@ +# Natural Language Toolkit: Regular Expression Chunkers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# URL: +# For license information, see LICENSE.TXT + +import re + +import regex + +from nltk.chunk.api import ChunkParserI +from nltk.tree import Tree + +# ////////////////////////////////////////////////////// +# ChunkString +# ////////////////////////////////////////////////////// + + +class ChunkString: + """ + A string-based encoding of a particular chunking of a text. + Internally, the ``ChunkString`` class uses a single string to + encode the chunking of the input text. This string contains a + sequence of angle-bracket delimited tags, with chunking indicated + by braces. An example of this encoding is:: + + {
    }{
    }<.>{
    }<.> + + ``ChunkString`` are created from tagged texts (i.e., lists of + ``tokens`` whose type is ``TaggedType``). Initially, nothing is + chunked. + + The chunking of a ``ChunkString`` can be modified with the ``xform()`` + method, which uses a regular expression to transform the string + representation. These transformations should only add and remove + braces; they should *not* modify the sequence of angle-bracket + delimited tags. + + :type _str: str + :ivar _str: The internal string representation of the text's + encoding. This string representation contains a sequence of + angle-bracket delimited tags, with chunking indicated by + braces. An example of this encoding is:: + + {
    }{
    }<.>{
    }<.> + + :type _pieces: list(tagged tokens and chunks) + :ivar _pieces: The tagged tokens and chunks encoded by this ``ChunkString``. + :ivar _debug: The debug level. See the constructor docs. + + :cvar IN_CHUNK_PATTERN: A zero-width regexp pattern string that + will only match positions that are in chunks. + :cvar IN_STRIP_PATTERN: A zero-width regexp pattern string that + will only match positions that are in strips. + """ + + CHUNK_TAG_CHAR = r"[^\{\}<>]" + CHUNK_TAG = r"(<%s+?>)" % CHUNK_TAG_CHAR + + IN_CHUNK_PATTERN = r"(?=[^\{]*\})" + IN_STRIP_PATTERN = r"(?=[^\}]*(\{|$))" + + # These are used by _verify + _CHUNK = r"(\{%s+?\})+?" % CHUNK_TAG + _STRIP = r"(%s+?)+?" % CHUNK_TAG + _VALID = re.compile(r"^(\{?%s\}?)*?$" % CHUNK_TAG) + _BRACKETS = re.compile(r"[^\{\}]+") + _BALANCED_BRACKETS = re.compile(r"(\{\})*$") + + def __init__(self, chunk_struct, debug_level=1): + """ + Construct a new ``ChunkString`` that encodes the chunking of + the text ``tagged_tokens``. + + :type chunk_struct: Tree + :param chunk_struct: The chunk structure to be further chunked. + :type debug_level: int + :param debug_level: The level of debugging which should be + applied to transformations on the ``ChunkString``. The + valid levels are: + + - 0: no checks + - 1: full check on to_chunkstruct + - 2: full check on to_chunkstruct and cursory check after + each transformation. + - 3: full check on to_chunkstruct and full check after + each transformation. + + We recommend you use at least level 1. You should + probably use level 3 if you use any non-standard + subclasses of ``RegexpChunkRule``. + """ + self._root_label = chunk_struct.label() + self._pieces = chunk_struct[:] + tags = [self._tag(tok) for tok in self._pieces] + self._str = "<" + "><".join(tags) + ">" + self._debug = debug_level + + def _tag(self, tok): + if isinstance(tok, tuple): + return tok[1] + elif isinstance(tok, Tree): + return tok.label() + else: + raise ValueError("chunk structures must contain tagged " "tokens or trees") + + def _verify(self, s, verify_tags): + """ + Check to make sure that ``s`` still corresponds to some chunked + version of ``_pieces``. + + :type verify_tags: bool + :param verify_tags: Whether the individual tags should be + checked. If this is false, ``_verify`` will check to make + sure that ``_str`` encodes a chunked version of *some* + list of tokens. If this is true, then ``_verify`` will + check to make sure that the tags in ``_str`` match those in + ``_pieces``. + + :raise ValueError: if the internal string representation of + this ``ChunkString`` is invalid or not consistent with _pieces. + """ + # Check overall form + if not ChunkString._VALID.match(s): + raise ValueError( + "Transformation generated invalid " "chunkstring:\n %s" % s + ) + + # Check that parens are balanced. If the string is long, we + # have to do this in pieces, to avoid a maximum recursion + # depth limit for regular expressions. + brackets = ChunkString._BRACKETS.sub("", s) + for i in range(1 + len(brackets) // 5000): + substr = brackets[i * 5000 : i * 5000 + 5000] + if not ChunkString._BALANCED_BRACKETS.match(substr): + raise ValueError( + "Transformation generated invalid " "chunkstring:\n %s" % s + ) + + if verify_tags <= 0: + return + + tags1 = (re.split(r"[\{\}<>]+", s))[1:-1] + tags2 = [self._tag(piece) for piece in self._pieces] + if tags1 != tags2: + raise ValueError( + "Transformation generated invalid " "chunkstring: tag changed" + ) + + def to_chunkstruct(self, chunk_label="CHUNK"): + """ + Return the chunk structure encoded by this ``ChunkString``. + + :rtype: Tree + :raise ValueError: If a transformation has generated an + invalid chunkstring. + """ + if self._debug > 0: + self._verify(self._str, 1) + + # Use this alternating list to create the chunkstruct. + pieces = [] + index = 0 + piece_in_chunk = 0 + for piece in re.split("[{}]", self._str): + # Find the list of tokens contained in this piece. + length = piece.count("<") + subsequence = self._pieces[index : index + length] + + # Add this list of tokens to our pieces. + if piece_in_chunk: + pieces.append(Tree(chunk_label, subsequence)) + else: + pieces += subsequence + + # Update index, piece_in_chunk + index += length + piece_in_chunk = not piece_in_chunk + + return Tree(self._root_label, pieces) + + def xform(self, regexp, repl): + """ + Apply the given transformation to the string encoding of this + ``ChunkString``. In particular, find all occurrences that match + ``regexp``, and replace them using ``repl`` (as done by + ``re.sub``). + + This transformation should only add and remove braces; it + should *not* modify the sequence of angle-bracket delimited + tags. Furthermore, this transformation may not result in + improper bracketing. Note, in particular, that bracketing may + not be nested. + + :type regexp: str or regexp + :param regexp: A regular expression matching the substring + that should be replaced. This will typically include a + named group, which can be used by ``repl``. + :type repl: str + :param repl: An expression specifying what should replace the + matched substring. Typically, this will include a named + replacement group, specified by ``regexp``. + :rtype: None + :raise ValueError: If this transformation generated an + invalid chunkstring. + """ + # Do the actual substitution + s = re.sub(regexp, repl, self._str) + + # The substitution might have generated "empty chunks" + # (substrings of the form "{}"). Remove them, so they don't + # interfere with other transformations. + s = re.sub(r"\{\}", "", s) + + # Make sure that the transformation was legal. + if self._debug > 1: + self._verify(s, self._debug - 2) + + # Commit the transformation. + self._str = s + + def __repr__(self): + """ + Return a string representation of this ``ChunkString``. + It has the form:: + + }{
    }'> + + :rtype: str + """ + return "" % repr(self._str) + + def __str__(self): + """ + Return a formatted representation of this ``ChunkString``. + This representation will include extra spaces to ensure that + tags will line up with the representation of other + ``ChunkStrings`` for the same text, regardless of the chunking. + + :rtype: str + """ + # Add spaces to make everything line up. + str = re.sub(r">(?!\})", r"> ", self._str) + str = re.sub(r"([^\{])<", r"\1 <", str) + if str[0] == "<": + str = " " + str + return str + + +# ////////////////////////////////////////////////////// +# Chunking Rules +# ////////////////////////////////////////////////////// + + +class RegexpChunkRule: + """ + A rule specifying how to modify the chunking in a ``ChunkString``, + using a transformational regular expression. The + ``RegexpChunkRule`` class itself can be used to implement any + transformational rule based on regular expressions. There are + also a number of subclasses, which can be used to implement + simpler types of rules, based on matching regular expressions. + + Each ``RegexpChunkRule`` has a regular expression and a + replacement expression. When a ``RegexpChunkRule`` is "applied" + to a ``ChunkString``, it searches the ``ChunkString`` for any + substring that matches the regular expression, and replaces it + using the replacement expression. This search/replace operation + has the same semantics as ``re.sub``. + + Each ``RegexpChunkRule`` also has a description string, which + gives a short (typically less than 75 characters) description of + the purpose of the rule. + + This transformation defined by this ``RegexpChunkRule`` should + only add and remove braces; it should *not* modify the sequence + of angle-bracket delimited tags. Furthermore, this transformation + may not result in nested or mismatched bracketing. + """ + + def __init__(self, regexp, repl, descr): + """ + Construct a new RegexpChunkRule. + + :type regexp: regexp or str + :param regexp: The regular expression for this ``RegexpChunkRule``. + When this rule is applied to a ``ChunkString``, any + substring that matches ``regexp`` will be replaced using + the replacement string ``repl``. Note that this must be a + normal regular expression, not a tag pattern. + :type repl: str + :param repl: The replacement expression for this ``RegexpChunkRule``. + When this rule is applied to a ``ChunkString``, any substring + that matches ``regexp`` will be replaced using ``repl``. + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + if isinstance(regexp, str): + regexp = re.compile(regexp) + self._repl = repl + self._descr = descr + self._regexp = regexp + + def apply(self, chunkstr): + # Keep docstring generic so we can inherit it. + """ + Apply this rule to the given ``ChunkString``. See the + class reference documentation for a description of what it + means to apply a rule. + + :type chunkstr: ChunkString + :param chunkstr: The chunkstring to which this rule is applied. + :rtype: None + :raise ValueError: If this transformation generated an + invalid chunkstring. + """ + chunkstr.xform(self._regexp, self._repl) + + def descr(self): + """ + Return a short description of the purpose and/or effect of + this rule. + + :rtype: str + """ + return self._descr + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + }'->''> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return ( + "" + + repr(self._repl) + + ">" + ) + + @staticmethod + def fromstring(s): + """ + Create a RegexpChunkRule from a string description. + Currently, the following formats are supported:: + + {regexp} # chunk rule + }regexp{ # strip rule + regexp}{regexp # split rule + regexp{}regexp # merge rule + + Where ``regexp`` is a regular expression for the rule. Any + text following the comment marker (``#``) will be used as + the rule's description: + + >>> from nltk.chunk.regexp import RegexpChunkRule + >>> RegexpChunkRule.fromstring('{
    ?+}') + ?+'> + """ + # Split off the comment (but don't split on '\#') + m = re.match(r"(?P(\\.|[^#])*)(?P#.*)?", s) + rule = m.group("rule").strip() + comment = (m.group("comment") or "")[1:].strip() + + # Pattern bodies: chunk, strip, split, merge + try: + if not rule: + raise ValueError("Empty chunk pattern") + if rule[0] == "{" and rule[-1] == "}": + return ChunkRule(rule[1:-1], comment) + elif rule[0] == "}" and rule[-1] == "{": + return StripRule(rule[1:-1], comment) + elif "}{" in rule: + left, right = rule.split("}{") + return SplitRule(left, right, comment) + elif "{}" in rule: + left, right = rule.split("{}") + return MergeRule(left, right, comment) + elif re.match("[^{}]*{[^{}]*}[^{}]*", rule): + left, chunk, right = re.split("[{}]", rule) + return ChunkRuleWithContext(left, chunk, right, comment) + else: + raise ValueError("Illegal chunk pattern: %s" % rule) + except (ValueError, re.error) as e: + raise ValueError("Illegal chunk pattern: %s" % rule) from e + + +class ChunkRule(RegexpChunkRule): + """ + A rule specifying how to add chunks to a ``ChunkString``, using a + matching tag pattern. When applied to a ``ChunkString``, it will + find any substring that matches this tag pattern and that is not + already part of a chunk, and create a new chunk containing that + substring. + """ + + def __init__(self, tag_pattern, descr): + """ + Construct a new ``ChunkRule``. + + :type tag_pattern: str + :param tag_pattern: This rule's tag pattern. When + applied to a ``ChunkString``, this rule will + chunk any substring that matches this tag pattern and that + is not already part of a chunk. + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + self._pattern = tag_pattern + regexp = re.compile( + "(?P%s)%s" + % (tag_pattern2re_pattern(tag_pattern), ChunkString.IN_STRIP_PATTERN) + ) + RegexpChunkRule.__init__(self, regexp, r"{\g}", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + '> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return "" + + +class StripRule(RegexpChunkRule): + """ + A rule specifying how to remove strips to a ``ChunkString``, + using a matching tag pattern. When applied to a + ``ChunkString``, it will find any substring that matches this + tag pattern and that is contained in a chunk, and remove it + from that chunk, thus creating two new chunks. + """ + + def __init__(self, tag_pattern, descr): + """ + Construct a new ``StripRule``. + + :type tag_pattern: str + :param tag_pattern: This rule's tag pattern. When + applied to a ``ChunkString``, this rule will + find any substring that matches this tag pattern and that + is contained in a chunk, and remove it from that chunk, + thus creating two new chunks. + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + self._pattern = tag_pattern + regexp = re.compile( + "(?P%s)%s" + % (tag_pattern2re_pattern(tag_pattern), ChunkString.IN_CHUNK_PATTERN) + ) + RegexpChunkRule.__init__(self, regexp, r"}\g{", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + '> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return "" + + +class UnChunkRule(RegexpChunkRule): + """ + A rule specifying how to remove chunks to a ``ChunkString``, + using a matching tag pattern. When applied to a + ``ChunkString``, it will find any complete chunk that matches this + tag pattern, and un-chunk it. + """ + + def __init__(self, tag_pattern, descr): + """ + Construct a new ``UnChunkRule``. + + :type tag_pattern: str + :param tag_pattern: This rule's tag pattern. When + applied to a ``ChunkString``, this rule will + find any complete chunk that matches this tag pattern, + and un-chunk it. + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + self._pattern = tag_pattern + regexp = re.compile(r"\{(?P%s)\}" % tag_pattern2re_pattern(tag_pattern)) + RegexpChunkRule.__init__(self, regexp, r"\g", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + '> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return "" + + +class MergeRule(RegexpChunkRule): + """ + A rule specifying how to merge chunks in a ``ChunkString``, using + two matching tag patterns: a left pattern, and a right pattern. + When applied to a ``ChunkString``, it will find any chunk whose end + matches left pattern, and immediately followed by a chunk whose + beginning matches right pattern. It will then merge those two + chunks into a single chunk. + """ + + def __init__(self, left_tag_pattern, right_tag_pattern, descr): + """ + Construct a new ``MergeRule``. + + :type right_tag_pattern: str + :param right_tag_pattern: This rule's right tag + pattern. When applied to a ``ChunkString``, this + rule will find any chunk whose end matches + ``left_tag_pattern``, and immediately followed by a chunk + whose beginning matches this pattern. It will + then merge those two chunks into a single chunk. + :type left_tag_pattern: str + :param left_tag_pattern: This rule's left tag + pattern. When applied to a ``ChunkString``, this + rule will find any chunk whose end matches + this pattern, and immediately followed by a chunk + whose beginning matches ``right_tag_pattern``. It will + then merge those two chunks into a single chunk. + + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + # Ensure that the individual patterns are coherent. E.g., if + # left='(' and right=')', then this will raise an exception: + re.compile(tag_pattern2re_pattern(left_tag_pattern)) + re.compile(tag_pattern2re_pattern(right_tag_pattern)) + + self._left_tag_pattern = left_tag_pattern + self._right_tag_pattern = right_tag_pattern + regexp = re.compile( + "(?P%s)}{(?=%s)" + % ( + tag_pattern2re_pattern(left_tag_pattern), + tag_pattern2re_pattern(right_tag_pattern), + ) + ) + RegexpChunkRule.__init__(self, regexp, r"\g", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + ', ''> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return ( + "" + ) + + +class SplitRule(RegexpChunkRule): + """ + A rule specifying how to split chunks in a ``ChunkString``, using + two matching tag patterns: a left pattern, and a right pattern. + When applied to a ``ChunkString``, it will find any chunk that + matches the left pattern followed by the right pattern. It will + then split the chunk into two new chunks, at the point between the + two pattern matches. + """ + + def __init__(self, left_tag_pattern, right_tag_pattern, descr): + """ + Construct a new ``SplitRule``. + + :type right_tag_pattern: str + :param right_tag_pattern: This rule's right tag + pattern. When applied to a ``ChunkString``, this rule will + find any chunk containing a substring that matches + ``left_tag_pattern`` followed by this pattern. It will + then split the chunk into two new chunks at the point + between these two matching patterns. + :type left_tag_pattern: str + :param left_tag_pattern: This rule's left tag + pattern. When applied to a ``ChunkString``, this rule will + find any chunk containing a substring that matches this + pattern followed by ``right_tag_pattern``. It will then + split the chunk into two new chunks at the point between + these two matching patterns. + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + # Ensure that the individual patterns are coherent. E.g., if + # left='(' and right=')', then this will raise an exception: + re.compile(tag_pattern2re_pattern(left_tag_pattern)) + re.compile(tag_pattern2re_pattern(right_tag_pattern)) + + self._left_tag_pattern = left_tag_pattern + self._right_tag_pattern = right_tag_pattern + regexp = re.compile( + "(?P%s)(?=%s)" + % ( + tag_pattern2re_pattern(left_tag_pattern), + tag_pattern2re_pattern(right_tag_pattern), + ) + ) + RegexpChunkRule.__init__(self, regexp, r"\g}{", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + ', '
    '> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return ( + "" + ) + + +class ExpandLeftRule(RegexpChunkRule): + """ + A rule specifying how to expand chunks in a ``ChunkString`` to the left, + using two matching tag patterns: a left pattern, and a right pattern. + When applied to a ``ChunkString``, it will find any chunk whose beginning + matches right pattern, and immediately preceded by a strip whose + end matches left pattern. It will then expand the chunk to incorporate + the new material on the left. + """ + + def __init__(self, left_tag_pattern, right_tag_pattern, descr): + """ + Construct a new ``ExpandRightRule``. + + :type right_tag_pattern: str + :param right_tag_pattern: This rule's right tag + pattern. When applied to a ``ChunkString``, this + rule will find any chunk whose beginning matches + ``right_tag_pattern``, and immediately preceded by a strip + whose end matches this pattern. It will + then merge those two chunks into a single chunk. + :type left_tag_pattern: str + :param left_tag_pattern: This rule's left tag + pattern. When applied to a ``ChunkString``, this + rule will find any chunk whose beginning matches + this pattern, and immediately preceded by a strip + whose end matches ``left_tag_pattern``. It will + then expand the chunk to incorporate the new material on the left. + + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + # Ensure that the individual patterns are coherent. E.g., if + # left='(' and right=')', then this will raise an exception: + re.compile(tag_pattern2re_pattern(left_tag_pattern)) + re.compile(tag_pattern2re_pattern(right_tag_pattern)) + + self._left_tag_pattern = left_tag_pattern + self._right_tag_pattern = right_tag_pattern + regexp = re.compile( + r"(?P%s)\{(?P%s)" + % ( + tag_pattern2re_pattern(left_tag_pattern), + tag_pattern2re_pattern(right_tag_pattern), + ) + ) + RegexpChunkRule.__init__(self, regexp, r"{\g\g", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + ', ''> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return ( + "" + ) + + +class ExpandRightRule(RegexpChunkRule): + """ + A rule specifying how to expand chunks in a ``ChunkString`` to the + right, using two matching tag patterns: a left pattern, and a + right pattern. When applied to a ``ChunkString``, it will find any + chunk whose end matches left pattern, and immediately followed by + a strip whose beginning matches right pattern. It will then + expand the chunk to incorporate the new material on the right. + """ + + def __init__(self, left_tag_pattern, right_tag_pattern, descr): + """ + Construct a new ``ExpandRightRule``. + + :type right_tag_pattern: str + :param right_tag_pattern: This rule's right tag + pattern. When applied to a ``ChunkString``, this + rule will find any chunk whose end matches + ``left_tag_pattern``, and immediately followed by a strip + whose beginning matches this pattern. It will + then merge those two chunks into a single chunk. + :type left_tag_pattern: str + :param left_tag_pattern: This rule's left tag + pattern. When applied to a ``ChunkString``, this + rule will find any chunk whose end matches + this pattern, and immediately followed by a strip + whose beginning matches ``right_tag_pattern``. It will + then expand the chunk to incorporate the new material on the right. + + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + # Ensure that the individual patterns are coherent. E.g., if + # left='(' and right=')', then this will raise an exception: + re.compile(tag_pattern2re_pattern(left_tag_pattern)) + re.compile(tag_pattern2re_pattern(right_tag_pattern)) + + self._left_tag_pattern = left_tag_pattern + self._right_tag_pattern = right_tag_pattern + regexp = re.compile( + r"(?P%s)\}(?P%s)" + % ( + tag_pattern2re_pattern(left_tag_pattern), + tag_pattern2re_pattern(right_tag_pattern), + ) + ) + RegexpChunkRule.__init__(self, regexp, r"\g\g}", descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + ', ''> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return ( + "" + ) + + +class ChunkRuleWithContext(RegexpChunkRule): + """ + A rule specifying how to add chunks to a ``ChunkString``, using + three matching tag patterns: one for the left context, one for the + chunk, and one for the right context. When applied to a + ``ChunkString``, it will find any substring that matches the chunk + tag pattern, is surrounded by substrings that match the two + context patterns, and is not already part of a chunk; and create a + new chunk containing the substring that matched the chunk tag + pattern. + + Caveat: Both the left and right context are consumed when this + rule matches; therefore, if you need to find overlapping matches, + you will need to apply your rule more than once. + """ + + def __init__( + self, + left_context_tag_pattern, + chunk_tag_pattern, + right_context_tag_pattern, + descr, + ): + """ + Construct a new ``ChunkRuleWithContext``. + + :type left_context_tag_pattern: str + :param left_context_tag_pattern: A tag pattern that must match + the left context of ``chunk_tag_pattern`` for this rule to + apply. + :type chunk_tag_pattern: str + :param chunk_tag_pattern: A tag pattern that must match for this + rule to apply. If the rule does apply, then this pattern + also identifies the substring that will be made into a chunk. + :type right_context_tag_pattern: str + :param right_context_tag_pattern: A tag pattern that must match + the right context of ``chunk_tag_pattern`` for this rule to + apply. + :type descr: str + :param descr: A short description of the purpose and/or effect + of this rule. + """ + # Ensure that the individual patterns are coherent. E.g., if + # left='(' and right=')', then this will raise an exception: + re.compile(tag_pattern2re_pattern(left_context_tag_pattern)) + re.compile(tag_pattern2re_pattern(chunk_tag_pattern)) + re.compile(tag_pattern2re_pattern(right_context_tag_pattern)) + + self._left_context_tag_pattern = left_context_tag_pattern + self._chunk_tag_pattern = chunk_tag_pattern + self._right_context_tag_pattern = right_context_tag_pattern + regexp = re.compile( + "(?P%s)(?P%s)(?P%s)%s" + % ( + tag_pattern2re_pattern(left_context_tag_pattern), + tag_pattern2re_pattern(chunk_tag_pattern), + tag_pattern2re_pattern(right_context_tag_pattern), + ChunkString.IN_STRIP_PATTERN, + ) + ) + replacement = r"\g{\g}\g" + RegexpChunkRule.__init__(self, regexp, replacement, descr) + + def __repr__(self): + """ + Return a string representation of this rule. It has the form:: + + ', '', '
    '> + + Note that this representation does not include the + description string; that string can be accessed + separately with the ``descr()`` method. + + :rtype: str + """ + return "".format( + self._left_context_tag_pattern, + self._chunk_tag_pattern, + self._right_context_tag_pattern, + ) + + +# ////////////////////////////////////////////////////// +# Tag Pattern Format Conversion +# ////////////////////////////////////////////////////// + +# this should probably be made more strict than it is -- e.g., it +# currently accepts 'foo'. +# +# The first alternative must NOT be quantified with "+" here: nesting it +# inside the outer "( ... )*" gives the classic ``(A+)*`` shape, which causes +# catastrophic (exponential) backtracking when the overall match fails on a +# trailing "{", "}", "<" or ">" (ReDoS, CWE-1333). Dropping the inner "+" is +# behaviour-preserving -- ``(A+|)*`` and ``(A|)*`` accept exactly the +# same language, because a run of ``A``s is just several single-``A`` iterations +# of the outer star -- while making the scan linear. See huntr report +# https://huntr.com/bounties/aff8ef29-2f20-46a4-ae13-7ce6010e26a5. +CHUNK_TAG_PATTERN = re.compile( + r"^(({}|<{}>)*)$".format(r"([^\{\}<>]|\{\d+,?\}|\{\d*,\d+\})", r"[^\{\}<>]+") +) + + +def tag_pattern2re_pattern(tag_pattern): + """ + Convert a tag pattern to a regular expression pattern. A "tag + pattern" is a modified version of a regular expression, designed + for matching sequences of tags. The differences between regular + expression patterns and tag patterns are: + + - In tag patterns, ``'<'`` and ``'>'`` act as parentheses; so + ``'+'`` matches one or more repetitions of ``''``, not + ``''``. + - Whitespace in tag patterns is ignored. So + ``'
    | '`` is equivalent to ``'
    |'`` + - In tag patterns, ``'.'`` is equivalent to ``'[^{}<>]'``; so + ``''`` matches any single tag starting with ``'NN'``. + + In particular, ``tag_pattern2re_pattern`` performs the following + transformations on the given pattern: + + - Replace '.' with '[^<>{}]' + - Remove any whitespace + - Add extra parens around '<' and '>', to make '<' and '>' act + like parentheses. E.g., so that in '+', the '+' has scope + over the entire ''; and so that in '', the '|' has + scope over 'NN' and 'IN', but not '<' or '>'. + - Check to make sure the resulting pattern is valid. + + :type tag_pattern: str + :param tag_pattern: The tag pattern to convert to a regular + expression pattern. + :raise ValueError: If ``tag_pattern`` is not a valid tag pattern. + In particular, ``tag_pattern`` should not include braces; and it + should not contain nested or mismatched angle-brackets. + :rtype: str + :return: A regular expression pattern corresponding to + ``tag_pattern``. + """ + # Clean up the regular expression + tag_pattern = re.sub(r"\s", "", tag_pattern) + tag_pattern = re.sub(r"<", "(<(", tag_pattern) + tag_pattern = re.sub(r">", ")>)", tag_pattern) + + # Check the regular expression + if not CHUNK_TAG_PATTERN.match(tag_pattern): + raise ValueError("Bad tag pattern: %r" % tag_pattern) + + # Replace "." with CHUNK_TAG_CHAR. + # We have to do this after, since it adds {}[]<>s, which would + # confuse CHUNK_TAG_PATTERN. + # PRE doesn't have lookback assertions, so reverse twice, and do + # the pattern backwards (with lookahead assertions). This can be + # made much cleaner once we can switch back to SRE. + def reverse_str(str): + lst = list(str) + lst.reverse() + return "".join(lst) + + tc_rev = reverse_str(ChunkString.CHUNK_TAG_CHAR) + reversed = reverse_str(tag_pattern) + reversed = re.sub(r"\.(?!\\(\\\\)*($|[^\\]))", tc_rev, reversed) + tag_pattern = reverse_str(reversed) + + return tag_pattern + + +# ////////////////////////////////////////////////////// +# RegexpChunkParser +# ////////////////////////////////////////////////////// + + +class RegexpChunkParser(ChunkParserI): + """ + A regular expression based chunk parser. ``RegexpChunkParser`` uses a + sequence of "rules" to find chunks of a single type within a + text. The chunking of the text is encoded using a ``ChunkString``, + and each rule acts by modifying the chunking in the + ``ChunkString``. The rules are all implemented using regular + expression matching and substitution. + + The ``RegexpChunkRule`` class and its subclasses (``ChunkRule``, + ``StripRule``, ``UnChunkRule``, ``MergeRule``, and ``SplitRule``) + define the rules that are used by ``RegexpChunkParser``. Each rule + defines an ``apply()`` method, which modifies the chunking encoded + by a given ``ChunkString``. + + :type _rules: list(RegexpChunkRule) + :ivar _rules: The list of rules that should be applied to a text. + :type _trace: int + :ivar _trace: The default level of tracing. + + """ + + def __init__(self, rules, chunk_label="NP", root_label="S", trace=0): + """ + Construct a new ``RegexpChunkParser``. + + :type rules: list(RegexpChunkRule) + :param rules: The sequence of rules that should be used to + generate the chunking for a tagged text. + :type chunk_label: str + :param chunk_label: The node value that should be used for + chunk subtrees. This is typically a short string + describing the type of information contained by the chunk, + such as ``"NP"`` for base noun phrases. + :type root_label: str + :param root_label: The node value that should be used for the + top node of the chunk structure. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + ``1`` will generate normal tracing output; and ``2`` or + higher will generate verbose tracing output. + """ + self._rules = rules + self._trace = trace + self._chunk_label = chunk_label + self._root_label = root_label + + def _trace_apply(self, chunkstr, verbose): + """ + Apply each rule of this ``RegexpChunkParser`` to ``chunkstr``, in + turn. Generate trace output between each rule. If ``verbose`` + is true, then generate verbose output. + + :type chunkstr: ChunkString + :param chunkstr: The chunk string to which each rule should be + applied. + :type verbose: bool + :param verbose: Whether output should be verbose. + :rtype: None + """ + print("# Input:") + print(chunkstr) + for rule in self._rules: + rule.apply(chunkstr) + if verbose: + print("#", rule.descr() + " (" + repr(rule) + "):") + else: + print("#", rule.descr() + ":") + print(chunkstr) + + def _notrace_apply(self, chunkstr): + """ + Apply each rule of this ``RegexpChunkParser`` to ``chunkstr``, in + turn. + + :param chunkstr: The chunk string to which each rule should be + applied. + :type chunkstr: ChunkString + :rtype: None + """ + + for rule in self._rules: + rule.apply(chunkstr) + + def parse(self, chunk_struct, trace=None): + """ + :type chunk_struct: Tree + :param chunk_struct: the chunk structure to be (further) chunked + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + ``1`` will generate normal tracing output; and ``2`` or + higher will generate verbose tracing output. This value + overrides the trace level value that was given to the + constructor. + :rtype: Tree + :return: a chunk structure that encodes the chunks in a given + tagged sentence. A chunk is a non-overlapping linguistic + group, such as a noun phrase. The set of chunks + identified in the chunk structure depends on the rules + used to define this ``RegexpChunkParser``. + """ + if len(chunk_struct) == 0: + print("Warning: parsing empty text") + return Tree(self._root_label, []) + + try: + chunk_struct.label() + except AttributeError: + chunk_struct = Tree(self._root_label, chunk_struct) + + # Use the default trace value? + if trace is None: + trace = self._trace + + chunkstr = ChunkString(chunk_struct) + + # Apply the sequence of rules to the chunkstring. + if trace: + verbose = trace > 1 + self._trace_apply(chunkstr, verbose) + else: + self._notrace_apply(chunkstr) + + # Use the chunkstring to create a chunk structure. + return chunkstr.to_chunkstruct(self._chunk_label) + + def rules(self): + """ + :return: the sequence of rules used by ``RegexpChunkParser``. + :rtype: list(RegexpChunkRule) + """ + return self._rules + + def __repr__(self): + """ + :return: a concise string representation of this + ``RegexpChunkParser``. + :rtype: str + """ + return "" % len(self._rules) + + def __str__(self): + """ + :return: a verbose string representation of this ``RegexpChunkParser``. + :rtype: str + """ + s = "RegexpChunkParser with %d rules:\n" % len(self._rules) + margin = 0 + for rule in self._rules: + margin = max(margin, len(rule.descr())) + if margin < 35: + format = " %" + repr(-(margin + 3)) + "s%s\n" + else: + format = " %s\n %s\n" + for rule in self._rules: + s += format % (rule.descr(), repr(rule)) + return s[:-1] + + +# ////////////////////////////////////////////////////// +# Chunk Grammar +# ////////////////////////////////////////////////////// + + +class RegexpParser(ChunkParserI): + r""" + A grammar based chunk parser. ``chunk.RegexpParser`` uses a set of + regular expression patterns to specify the behavior of the parser. + The chunking of the text is encoded using a ``ChunkString``, and + each rule acts by modifying the chunking in the ``ChunkString``. + The rules are all implemented using regular expression matching + and substitution. + + A grammar contains one or more clauses in the following form:: + + NP: + {} # chunk determiners and adjectives + }<[\.VI].*>+{ # strip any tag beginning with V, I, or . + <.*>}{
    # split a chunk at a determiner + {} # merge chunk ending with det/adj + # with one starting with a noun + + The patterns of a clause are executed in order. An earlier + pattern may introduce a chunk boundary that prevents a later + pattern from executing. Sometimes an individual pattern will + match on multiple, overlapping extents of the input. As with + regular expression substitution more generally, the chunker will + identify the first match possible, then continue looking for matches + after this one has ended. + + The clauses of a grammar are also executed in order. A cascaded + chunk parser is one having more than one clause. The maximum depth + of a parse tree created by this chunk parser is the same as the + number of clauses in the grammar. + + When tracing is turned on, the comment portion of a line is displayed + each time the corresponding pattern is applied. + + :type _start: str + :ivar _start: The start symbol of the grammar (the root node of + resulting trees) + :type _stages: int + :ivar _stages: The list of parsing stages corresponding to the grammar + + """ + + def __init__(self, grammar, root_label="S", loop=1, trace=0): + """ + Create a new chunk parser, from the given start state + and set of chunk patterns. + + :param grammar: The grammar, or a list of RegexpChunkParser objects + :type grammar: str or list(RegexpChunkParser) + :param root_label: The top node of the tree being created + :type root_label: str or Nonterminal + :param loop: The number of times to run through the patterns + :type loop: int + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + ``1`` will generate normal tracing output; and ``2`` or + higher will generate verbose tracing output. + """ + self._trace = trace + self._stages = [] + self._grammar = grammar + self._loop = loop + + if isinstance(grammar, str): + self._read_grammar(grammar, root_label, trace) + else: + # Make sur the grammar looks like it has the right type: + type_err = ( + "Expected string or list of RegexpChunkParsers " "for the grammar." + ) + try: + grammar = list(grammar) + except BaseException as e: + raise TypeError(type_err) from e + for elt in grammar: + if not isinstance(elt, RegexpChunkParser): + raise TypeError(type_err) + self._stages = grammar + + def _read_grammar(self, grammar, root_label, trace): + """ + Helper function for __init__: read the grammar if it is a + string. + """ + rules = [] + lhs = None + pattern = regex.compile("(?P(\\.|[^:])*)(:(?P.*))") + for line in grammar.split("\n"): + line = line.strip() + + # New stage begins if there's an unescaped ':' + m = pattern.match(line) + if m: + # Record the stage that we just completed. + self._add_stage(rules, lhs, root_label, trace) + # Start a new stage. + lhs = m.group("nonterminal").strip() + rules = [] + line = m.group("rule").strip() + + # Skip blank & comment-only lines + if line == "" or line.startswith("#"): + continue + + # Add the rule + rules.append(RegexpChunkRule.fromstring(line)) + + # Record the final stage + self._add_stage(rules, lhs, root_label, trace) + + def _add_stage(self, rules, lhs, root_label, trace): + """ + Helper function for __init__: add a new stage to the parser. + """ + if rules != []: + if not lhs: + raise ValueError("Expected stage marker (eg NP:)") + parser = RegexpChunkParser( + rules, chunk_label=lhs, root_label=root_label, trace=trace + ) + self._stages.append(parser) + + def parse(self, chunk_struct, trace=None): + """ + Apply the chunk parser to this input. + + :type chunk_struct: Tree + :param chunk_struct: the chunk structure to be (further) chunked + (this tree is modified, and is also returned) + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + ``1`` will generate normal tracing output; and ``2`` or + higher will generate verbose tracing output. This value + overrides the trace level value that was given to the + constructor. + :return: the chunked output. + :rtype: Tree + """ + if trace is None: + trace = self._trace + for i in range(self._loop): + for parser in self._stages: + chunk_struct = parser.parse(chunk_struct, trace=trace) + return chunk_struct + + def __repr__(self): + """ + :return: a concise string representation of this ``chunk.RegexpParser``. + :rtype: str + """ + return "" % len(self._stages) + + def __str__(self): + """ + :return: a verbose string representation of this + ``RegexpParser``. + :rtype: str + """ + s = "chunk.RegexpParser with %d stages:\n" % len(self._stages) + margin = 0 + for parser in self._stages: + s += "%s\n" % parser + return s[:-1] + + +# ////////////////////////////////////////////////////// +# Demonstration code +# ////////////////////////////////////////////////////// + + +def demo_eval(chunkparser, text): + """ + Demonstration code for evaluating a chunk parser, using a + ``ChunkScore``. This function assumes that ``text`` contains one + sentence per line, and that each sentence has the form expected by + ``tree.chunk``. It runs the given chunk parser on each sentence in + the text, and scores the result. It prints the final score + (precision, recall, and f-measure); and reports the set of chunks + that were missed and the set of chunks that were incorrect. (At + most 10 missing chunks and 10 incorrect chunks are reported). + + :param chunkparser: The chunkparser to be tested + :type chunkparser: ChunkParserI + :param text: The chunked tagged text that should be used for + evaluation. + :type text: str + """ + from nltk import chunk + from nltk.tree import Tree + + # Evaluate our chunk parser. + chunkscore = chunk.ChunkScore() + + for sentence in text.split("\n"): + print(sentence) + sentence = sentence.strip() + if not sentence: + continue + gold = chunk.tagstr2tree(sentence) + tokens = gold.leaves() + test = chunkparser.parse(Tree("S", tokens), trace=1) + chunkscore.score(gold, test) + print() + + print("/" + ("=" * 75) + "\\") + print("Scoring", chunkparser) + print("-" * 77) + print("Precision: %5.1f%%" % (chunkscore.precision() * 100), " " * 4, end=" ") + print("Recall: %5.1f%%" % (chunkscore.recall() * 100), " " * 6, end=" ") + print("F-Measure: %5.1f%%" % (chunkscore.f_measure() * 100)) + + # Missed chunks. + if chunkscore.missed(): + print("Missed:") + missed = chunkscore.missed() + for chunk in missed[:10]: + print(" ", " ".join(map(str, chunk))) + if len(chunkscore.missed()) > 10: + print(" ...") + + # Incorrect chunks. + if chunkscore.incorrect(): + print("Incorrect:") + incorrect = chunkscore.incorrect() + for chunk in incorrect[:10]: + print(" ", " ".join(map(str, chunk))) + if len(chunkscore.incorrect()) > 10: + print(" ...") + + print("\\" + ("=" * 75) + "/") + print() + + +def demo(): + """ + A demonstration for the ``RegexpChunkParser`` class. A single text is + parsed with four different chunk parsers, using a variety of rules + and strategies. + """ + + from nltk import Tree, chunk + + text = """\ + [ the/DT little/JJ cat/NN ] sat/VBD on/IN [ the/DT mat/NN ] ./. + [ John/NNP ] saw/VBD [the/DT cats/NNS] [the/DT dog/NN] chased/VBD ./. + [ John/NNP ] thinks/VBZ [ Mary/NN ] saw/VBD [ the/DT cat/NN ] sit/VB on/IN [ the/DT mat/NN ]./. + """ + + print("*" * 75) + print("Evaluation text:") + print(text) + print("*" * 75) + print() + + grammar = r""" + NP: # NP stage + {
    ?*} # chunk determiners, adjectives and nouns + {+} # chunk proper nouns + """ + cp = chunk.RegexpParser(grammar) + demo_eval(cp, text) + + grammar = r""" + NP: + {<.*>} # start by chunking each tag + }<[\.VI].*>+{ # unchunk any verbs, prepositions or periods + {} # merge det/adj with nouns + """ + cp = chunk.RegexpParser(grammar) + demo_eval(cp, text) + + grammar = r""" + NP: {
    ?*} # chunk determiners, adjectives and nouns + VP: {?} # VP = verb words + """ + cp = chunk.RegexpParser(grammar) + demo_eval(cp, text) + + grammar = r""" + NP: {<.*>*} # start by chunking everything + }<[\.VI].*>+{ # strip any verbs, prepositions or periods + <.*>}{
    # separate on determiners + PP: {} # PP = preposition + noun phrase + VP: {*} # VP = verb words + NPs and PPs + """ + cp = chunk.RegexpParser(grammar) + demo_eval(cp, text) + + # Evaluation + + from nltk.corpus import conll2000 + + print() + print("Demonstration of empty grammar:") + + cp = chunk.RegexpParser("") + print(chunk.accuracy(cp, conll2000.chunked_sents("test.txt", chunk_types=("NP",)))) + + print() + print("Demonstration of accuracy evaluation using CoNLL tags:") + + grammar = r""" + NP: + {<.*>} # start by chunking each tag + }<[\.VI].*>+{ # unchunk any verbs, prepositions or periods + {} # merge det/adj with nouns + """ + cp = chunk.RegexpParser(grammar) + print(chunk.accuracy(cp, conll2000.chunked_sents("test.txt")[:5])) + + print() + print("Demonstration of tagged token input") + + grammar = r""" + NP: {<.*>*} # start by chunking everything + }<[\.VI].*>+{ # strip any verbs, prepositions or periods + <.*>}{
    # separate on determiners + PP: {} # PP = preposition + noun phrase + VP: {*} # VP = verb words + NPs and PPs + """ + cp = chunk.RegexpParser(grammar) + print( + cp.parse( + [ + ("the", "DT"), + ("little", "JJ"), + ("cat", "NN"), + ("sat", "VBD"), + ("on", "IN"), + ("the", "DT"), + ("mat", "NN"), + (".", "."), + ] + ) + ) + + +if __name__ == "__main__": + demo() diff --git a/nltk/chunk/util.py b/nltk/chunk/util.py new file mode 100644 index 0000000..d4120ed --- /dev/null +++ b/nltk/chunk/util.py @@ -0,0 +1,642 @@ +# Natural Language Toolkit: Chunk format conversions +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# URL: +# For license information, see LICENSE.TXT + +import re + +from nltk.metrics import accuracy as _accuracy +from nltk.tag.mapping import map_tag +from nltk.tag.util import str2tuple +from nltk.tree import Tree + +##////////////////////////////////////////////////////// +## EVALUATION +##////////////////////////////////////////////////////// + + +def accuracy(chunker, gold): + """ + Score the accuracy of the chunker against the gold standard. + Strip the chunk information from the gold standard and rechunk it using + the chunker, then compute the accuracy score. + + :type chunker: ChunkParserI + :param chunker: The chunker being evaluated. + :type gold: tree + :param gold: The chunk structures to score the chunker on. + :rtype: float + """ + + gold_tags = [] + test_tags = [] + for gold_tree in gold: + test_tree = chunker.parse(gold_tree.flatten()) + gold_tags += tree2conlltags(gold_tree) + test_tags += tree2conlltags(test_tree) + + # print 'GOLD:', gold_tags[:50] + # print 'TEST:', test_tags[:50] + return _accuracy(gold_tags, test_tags) + + +# Patched for increased performance by Yoav Goldberg , 2006-01-13 +# -- statistics are evaluated only on demand, instead of at every sentence evaluation +# +# SB: use nltk.metrics for precision/recall scoring? +# +class ChunkScore: + """ + A utility class for scoring chunk parsers. ``ChunkScore`` can + evaluate a chunk parser's output, based on a number of statistics + (precision, recall, f-measure, misssed chunks, incorrect chunks). + It can also combine the scores from the parsing of multiple texts; + this makes it significantly easier to evaluate a chunk parser that + operates one sentence at a time. + + Texts are evaluated with the ``score`` method. The results of + evaluation can be accessed via a number of accessor methods, such + as ``precision`` and ``f_measure``. A typical use of the + ``ChunkScore`` class is:: + + >>> chunkscore = ChunkScore() # doctest: +SKIP + >>> for correct in correct_sentences: # doctest: +SKIP + ... guess = chunkparser.parse(correct.leaves()) # doctest: +SKIP + ... chunkscore.score(correct, guess) # doctest: +SKIP + >>> print('F Measure:', chunkscore.f_measure()) # doctest: +SKIP + F Measure: 0.823 + + :ivar kwargs: Keyword arguments: + + - max_tp_examples: The maximum number actual examples of true + positives to record. This affects the ``correct`` member + function: ``correct`` will not return more than this number + of true positive examples. This does *not* affect any of + the numerical metrics (precision, recall, or f-measure) + + - max_fp_examples: The maximum number actual examples of false + positives to record. This affects the ``incorrect`` member + function and the ``guessed`` member function: ``incorrect`` + will not return more than this number of examples, and + ``guessed`` will not return more than this number of true + positive examples. This does *not* affect any of the + numerical metrics (precision, recall, or f-measure) + + - max_fn_examples: The maximum number actual examples of false + negatives to record. This affects the ``missed`` member + function and the ``correct`` member function: ``missed`` + will not return more than this number of examples, and + ``correct`` will not return more than this number of true + negative examples. This does *not* affect any of the + numerical metrics (precision, recall, or f-measure) + + - chunk_label: A regular expression indicating which chunks + should be compared. Defaults to ``'.*'`` (i.e., all chunks). + + :type _tp: list(Token) + :ivar _tp: List of true positives + :type _fp: list(Token) + :ivar _fp: List of false positives + :type _fn: list(Token) + :ivar _fn: List of false negatives + + :type _tp_num: int + :ivar _tp_num: Number of true positives + :type _fp_num: int + :ivar _fp_num: Number of false positives + :type _fn_num: int + :ivar _fn_num: Number of false negatives. + """ + + def __init__(self, **kwargs): + self._correct = set() + self._guessed = set() + self._tp = set() + self._fp = set() + self._fn = set() + self._max_tp = kwargs.get("max_tp_examples", 100) + self._max_fp = kwargs.get("max_fp_examples", 100) + self._max_fn = kwargs.get("max_fn_examples", 100) + self._chunk_label = kwargs.get("chunk_label", ".*") + self._tp_num = 0 + self._fp_num = 0 + self._fn_num = 0 + self._count = 0 + self._tags_correct = 0.0 + self._tags_total = 0.0 + + self._measuresNeedUpdate = False + + def _updateMeasures(self): + if self._measuresNeedUpdate: + self._tp = self._guessed & self._correct + self._fn = self._correct - self._guessed + self._fp = self._guessed - self._correct + self._tp_num = len(self._tp) + self._fp_num = len(self._fp) + self._fn_num = len(self._fn) + self._measuresNeedUpdate = False + + def score(self, correct, guessed): + """ + Given a correctly chunked sentence, score another chunked + version of the same sentence. + + :type correct: chunk structure + :param correct: The known-correct ("gold standard") chunked + sentence. + :type guessed: chunk structure + :param guessed: The chunked sentence to be scored. + """ + self._correct |= _chunksets(correct, self._count, self._chunk_label) + self._guessed |= _chunksets(guessed, self._count, self._chunk_label) + self._count += 1 + self._measuresNeedUpdate = True + # Keep track of per-tag accuracy (if possible) + try: + correct_tags = tree2conlltags(correct) + guessed_tags = tree2conlltags(guessed) + except ValueError: + # This exception case is for nested chunk structures, + # where tree2conlltags will fail with a ValueError: "Tree + # is too deeply nested to be printed in CoNLL format." + correct_tags = guessed_tags = () + self._tags_total += len(correct_tags) + self._tags_correct += sum( + 1 for (t, g) in zip(guessed_tags, correct_tags) if t == g + ) + + def accuracy(self): + """ + Return the overall tag-based accuracy for all text that have + been scored by this ``ChunkScore``, using the IOB (conll2000) + tag encoding. + + :rtype: float + """ + if self._tags_total == 0: + return 1 + return self._tags_correct / self._tags_total + + def precision(self): + """ + Return the overall precision for all texts that have been + scored by this ``ChunkScore``. + + :rtype: float + """ + self._updateMeasures() + div = self._tp_num + self._fp_num + if div == 0: + return 0 + else: + return self._tp_num / div + + def recall(self): + """ + Return the overall recall for all texts that have been + scored by this ``ChunkScore``. + + :rtype: float + """ + self._updateMeasures() + div = self._tp_num + self._fn_num + if div == 0: + return 0 + else: + return self._tp_num / div + + def f_measure(self, alpha=0.5): + """ + Return the overall F measure for all texts that have been + scored by this ``ChunkScore``. + + :param alpha: the relative weighting of precision and recall. + Larger alpha biases the score towards the precision value, + while smaller alpha biases the score towards the recall + value. ``alpha`` should have a value in the range [0,1]. + :type alpha: float + :rtype: float + """ + self._updateMeasures() + p = self.precision() + r = self.recall() + if p == 0 or r == 0: # what if alpha is 0 or 1? + return 0 + return 1 / (alpha / p + (1 - alpha) / r) + + def missed(self): + """ + Return the chunks which were included in the + correct chunk structures, but not in the guessed chunk + structures, listed in input order. + + :rtype: list of chunks + """ + self._updateMeasures() + chunks = list(self._fn) + return [c[1] for c in chunks] # discard position information + + def incorrect(self): + """ + Return the chunks which were included in the guessed chunk structures, + but not in the correct chunk structures, listed in input order. + + :rtype: list of chunks + """ + self._updateMeasures() + chunks = list(self._fp) + return [c[1] for c in chunks] # discard position information + + def correct(self): + """ + Return the chunks which were included in the correct + chunk structures, listed in input order. + + :rtype: list of chunks + """ + chunks = list(self._correct) + return [c[1] for c in chunks] # discard position information + + def guessed(self): + """ + Return the chunks which were included in the guessed + chunk structures, listed in input order. + + :rtype: list of chunks + """ + chunks = list(self._guessed) + return [c[1] for c in chunks] # discard position information + + def __len__(self): + self._updateMeasures() + return self._tp_num + self._fn_num + + def __repr__(self): + """ + Return a concise representation of this ``ChunkScoring``. + + :rtype: str + """ + return "" + + def __str__(self): + """ + Return a verbose representation of this ``ChunkScoring``. + This representation includes the precision, recall, and + f-measure scores. For other information about the score, + use the accessor methods (e.g., ``missed()`` and ``incorrect()``). + + :rtype: str + """ + return ( + "ChunkParse score:\n" + + f" IOB Accuracy: {self.accuracy() * 100:5.1f}%\n" + + f" Precision: {self.precision() * 100:5.1f}%\n" + + f" Recall: {self.recall() * 100:5.1f}%\n" + + f" F-Measure: {self.f_measure() * 100:5.1f}%" + ) + + +# extract chunks, and assign unique id, the absolute position of +# the first word of the chunk +def _chunksets(t, count, chunk_label): + pos = 0 + chunks = [] + for child in t: + if isinstance(child, Tree): + if re.match(chunk_label, child.label()): + chunks.append(((count, pos), child.freeze())) + pos += len(child.leaves()) + else: + pos += 1 + return set(chunks) + + +def tagstr2tree( + s, chunk_label="NP", root_label="S", sep="/", source_tagset=None, target_tagset=None +): + """ + Divide a string of bracketted tagged text into + chunks and unchunked tokens, and produce a Tree. + Chunks are marked by square brackets (``[...]``). Words are + delimited by whitespace, and each word should have the form + ``text/tag``. Words that do not contain a slash are + assigned a ``tag`` of None. + + :param s: The string to be converted + :type s: str + :param chunk_label: The label to use for chunk nodes + :type chunk_label: str + :param root_label: The label to use for the root of the tree + :type root_label: str + :rtype: Tree + """ + + WORD_OR_BRACKET = re.compile(r"\[|\]|[^\[\]\s]+") + + stack = [Tree(root_label, [])] + for match in WORD_OR_BRACKET.finditer(s): + text = match.group() + if text[0] == "[": + if len(stack) != 1: + raise ValueError(f"Unexpected [ at char {match.start():d}") + chunk = Tree(chunk_label, []) + stack[-1].append(chunk) + stack.append(chunk) + elif text[0] == "]": + if len(stack) != 2: + raise ValueError(f"Unexpected ] at char {match.start():d}") + stack.pop() + else: + if sep is None: + stack[-1].append(text) + else: + word, tag = str2tuple(text, sep) + if source_tagset and target_tagset: + tag = map_tag(source_tagset, target_tagset, tag) + stack[-1].append((word, tag)) + + if len(stack) != 1: + raise ValueError(f"Expected ] at char {len(s):d}") + return stack[0] + + +### CONLL + +_LINE_RE = re.compile(r"(\S+)\s+(\S+)\s+([IOB])-?(\S+)?") + + +def conllstr2tree(s, chunk_types=("NP", "PP", "VP"), root_label="S"): + """ + Return a chunk structure for a single sentence + encoded in the given CONLL 2000 style string. + This function converts a CoNLL IOB string into a tree. + It uses the specified chunk types + (defaults to NP, PP and VP), and creates a tree rooted at a node + labeled S (by default). + + :param s: The CoNLL string to be converted. + :type s: str + :param chunk_types: The chunk types to be converted. + :type chunk_types: tuple + :param root_label: The node label to use for the root. + :type root_label: str + :rtype: Tree + """ + + stack = [Tree(root_label, [])] + + for lineno, line in enumerate(s.split("\n")): + if not line.strip(): + continue + + # Decode the line. + match = _LINE_RE.match(line) + if match is None: + raise ValueError(f"Error on line {lineno:d}") + (word, tag, state, chunk_type) = match.groups() + + # If it's a chunk type we don't care about, treat it as O. + if chunk_types is not None and chunk_type not in chunk_types: + state = "O" + + # For "Begin"/"Outside", finish any completed chunks - + # also do so for "Inside" which don't match the previous token. + mismatch_I = state == "I" and chunk_type != stack[-1].label() + if state in "BO" or mismatch_I: + if len(stack) == 2: + stack.pop() + + # For "Begin", start a new chunk. + if state == "B" or mismatch_I: + chunk = Tree(chunk_type, []) + stack[-1].append(chunk) + stack.append(chunk) + + # Add the new word token. + stack[-1].append((word, tag)) + + return stack[0] + + +def tree2conlltags(t): + """ + Return a list of 3-tuples containing ``(word, tag, IOB-tag)``. + Convert a tree to the CoNLL IOB tag format. + + :param t: The tree to be converted. + :type t: Tree + :rtype: list(tuple) + """ + + tags = [] + for child in t: + try: + category = child.label() + prefix = "B-" + for contents in child: + if isinstance(contents, Tree): + raise ValueError( + "Tree is too deeply nested to be printed in CoNLL format" + ) + tags.append((contents[0], contents[1], prefix + category)) + prefix = "I-" + except AttributeError: + tags.append((child[0], child[1], "O")) + return tags + + +def conlltags2tree( + sentence, chunk_types=("NP", "PP", "VP"), root_label="S", strict=False +): + """ + Convert the CoNLL IOB format to a tree. + """ + tree = Tree(root_label, []) + for word, postag, chunktag in sentence: + if chunktag is None: + if strict: + raise ValueError("Bad conll tag sequence") + else: + # Treat as O + tree.append((word, postag)) + elif chunktag.startswith("B-"): + tree.append(Tree(chunktag[2:], [(word, postag)])) + elif chunktag.startswith("I-"): + if ( + len(tree) == 0 + or not isinstance(tree[-1], Tree) + or tree[-1].label() != chunktag[2:] + ): + if strict: + raise ValueError("Bad conll tag sequence") + else: + # Treat as B-* + tree.append(Tree(chunktag[2:], [(word, postag)])) + else: + tree[-1].append((word, postag)) + elif chunktag == "O": + tree.append((word, postag)) + else: + raise ValueError(f"Bad conll tag {chunktag!r}") + return tree + + +def tree2conllstr(t): + """ + Return a multiline string where each line contains a word, tag and IOB tag. + Convert a tree to the CoNLL IOB string format + + :param t: The tree to be converted. + :type t: Tree + :rtype: str + """ + lines = [" ".join(token) for token in tree2conlltags(t)] + return "\n".join(lines) + + +### IEER + +_IEER_DOC_RE = re.compile( + r"\s*" + r"(\s*(?P.+?)\s*\s*)?" + r"(\s*(?P.+?)\s*\s*)?" + r"(\s*(?P.+?)\s*\s*)?" + r"\s*" + r"(\s*(?P.+?)\s*\s*)?" + r"(?P.*?)\s*" + r"\s*\s*", + re.DOTALL, +) + +_IEER_TYPE_RE = re.compile(r']*?type="(?P\w+)"') + + +def _ieer_read_text(s, root_label): + stack = [Tree(root_label, [])] + # s will be None if there is no headline in the text + # return the empty list in place of a Tree + if s is None: + return [] + for piece_m in re.finditer(r"<[^>]+>|[^\s<]+", s): + piece = piece_m.group() + try: + if piece.startswith(".... + m = _IEER_DOC_RE.match(s) + if m: + return { + "text": _ieer_read_text(m.group("text"), root_label), + "docno": m.group("docno"), + "doctype": m.group("doctype"), + "date_time": m.group("date_time"), + #'headline': m.group('headline') + # we want to capture NEs in the headline too! + "headline": _ieer_read_text(m.group("headline"), root_label), + } + else: + return _ieer_read_text(s, root_label) + + +def demo(): + s = "[ Pierre/NNP Vinken/NNP ] ,/, [ 61/CD years/NNS ] old/JJ ,/, will/MD join/VB [ the/DT board/NN ] ./." + import nltk + + t = nltk.chunk.tagstr2tree(s, chunk_label="NP") + t.pprint() + print() + + s = """ +These DT B-NP +research NN I-NP +protocols NNS I-NP +offer VBP B-VP +to TO B-PP +the DT B-NP +patient NN I-NP +not RB O +only RB O +the DT B-NP +very RB I-NP +best JJS I-NP +therapy NN I-NP +which WDT B-NP +we PRP B-NP +have VBP B-VP +established VBN I-VP +today NN B-NP +but CC B-NP +also RB I-NP +the DT B-NP +hope NN I-NP +of IN B-PP +something NN B-NP +still RB B-ADJP +better JJR I-ADJP +. . O +""" + + conll_tree = conllstr2tree(s, chunk_types=("NP", "PP")) + conll_tree.pprint() + + # Demonstrate CoNLL output + print("CoNLL output:") + print(nltk.chunk.tree2conllstr(conll_tree)) + print() + + +if __name__ == "__main__": + demo() diff --git a/nltk/classify/__init__.py b/nltk/classify/__init__.py new file mode 100644 index 0000000..21c6002 --- /dev/null +++ b/nltk/classify/__init__.py @@ -0,0 +1,101 @@ +# Natural Language Toolkit: Classifiers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Classes and interfaces for labeling tokens with category labels (or +"class labels"). Typically, labels are represented with strings +(such as ``'health'`` or ``'sports'``). Classifiers can be used to +perform a wide range of classification tasks. For example, +classifiers can be used... + +- to classify documents by topic +- to classify ambiguous words by which word sense is intended +- to classify acoustic signals by which phoneme they represent +- to classify sentences by their author + +Features +======== +In order to decide which category label is appropriate for a given +token, classifiers examine one or more 'features' of the token. These +"features" are typically chosen by hand, and indicate which aspects +of the token are relevant to the classification decision. For +example, a document classifier might use a separate feature for each +word, recording how often that word occurred in the document. + +Featuresets +=========== +The features describing a token are encoded using a "featureset", +which is a dictionary that maps from "feature names" to "feature +values". Feature names are unique strings that indicate what aspect +of the token is encoded by the feature. Examples include +``'prevword'``, for a feature whose value is the previous word; and +``'contains-word(library)'`` for a feature that is true when a document +contains the word ``'library'``. Feature values are typically +booleans, numbers, or strings, depending on which feature they +describe. + +Featuresets are typically constructed using a "feature detector" +(also known as a "feature extractor"). A feature detector is a +function that takes a token (and sometimes information about its +context) as its input, and returns a featureset describing that token. +For example, the following feature detector converts a document +(stored as a list of words) to a featureset describing the set of +words included in the document: + + >>> # Define a feature detector function. + >>> def document_features(document): + ... return dict([('contains-word(%s)' % w, True) for w in document]) + +Feature detectors are typically applied to each token before it is fed +to the classifier: + + >>> # Classify each Gutenberg document. + >>> from nltk.corpus import gutenberg + >>> for fileid in gutenberg.fileids(): # doctest: +SKIP + ... doc = gutenberg.words(fileid) # doctest: +SKIP + ... print(fileid, classifier.classify(document_features(doc))) # doctest: +SKIP + +The parameters that a feature detector expects will vary, depending on +the task and the needs of the feature detector. For example, a +feature detector for word sense disambiguation (WSD) might take as its +input a sentence, and the index of a word that should be classified, +and return a featureset for that word. The following feature detector +for WSD includes features describing the left and right contexts of +the target word: + + >>> def wsd_features(sentence, index): + ... featureset = {} + ... for i in range(max(0, index-3), index): + ... featureset['left-context(%s)' % sentence[i]] = True + ... for i in range(index, max(index+3, len(sentence))): + ... featureset['right-context(%s)' % sentence[i]] = True + ... return featureset + +Training Classifiers +==================== +Most classifiers are built by training them on a list of hand-labeled +examples, known as the "training set". Training sets are represented +as lists of ``(featuredict, label)`` tuples. +""" + +from nltk.classify.api import ClassifierI, MultiClassifierI +from nltk.classify.decisiontree import DecisionTreeClassifier +from nltk.classify.maxent import ( + BinaryMaxentFeatureEncoding, + ConditionalExponentialClassifier, + MaxentClassifier, + TypedMaxentFeatureEncoding, +) +from nltk.classify.megam import call_megam, config_megam +from nltk.classify.naivebayes import NaiveBayesClassifier +from nltk.classify.positivenaivebayes import PositiveNaiveBayesClassifier +from nltk.classify.rte_classify import RTEFeatureExtractor, rte_classifier, rte_features +from nltk.classify.scikitlearn import SklearnClassifier +from nltk.classify.senna import Senna +from nltk.classify.textcat import TextCat +from nltk.classify.util import accuracy, apply_features, log_likelihood +from nltk.classify.weka import WekaClassifier, config_weka diff --git a/nltk/classify/api.py b/nltk/classify/api.py new file mode 100644 index 0000000..c3379fb --- /dev/null +++ b/nltk/classify/api.py @@ -0,0 +1,195 @@ +# Natural Language Toolkit: Classifier Interface +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# URL: +# For license information, see LICENSE.TXT + +""" +Interfaces for labeling tokens with category labels (or "class labels"). + +``ClassifierI`` is a standard interface for "single-category +classification", in which the set of categories is known, the number +of categories is finite, and each text belongs to exactly one +category. + +``MultiClassifierI`` is a standard interface for "multi-category +classification", which is like single-category classification except +that each text belongs to zero or more categories. +""" +from nltk.internals import overridden + +##////////////////////////////////////////////////////// +# { Classification Interfaces +##////////////////////////////////////////////////////// + + +class ClassifierI: + """ + A processing interface for labeling tokens with a single category + label (or "class"). Labels are typically strs or + ints, but can be any immutable type. The set of labels + that the classifier chooses from must be fixed and finite. + + Subclasses must define: + - ``labels()`` + - either ``classify()`` or ``classify_many()`` (or both) + + Subclasses may define: + - either ``prob_classify()`` or ``prob_classify_many()`` (or both) + """ + + def labels(self): + """ + :return: the list of category labels used by this classifier. + :rtype: list of (immutable) + """ + raise NotImplementedError() + + def classify(self, featureset): + """ + :return: the most appropriate label for the given featureset. + :rtype: label + """ + if overridden(self.classify_many): + return self.classify_many([featureset])[0] + else: + raise NotImplementedError() + + def prob_classify(self, featureset): + """ + :return: a probability distribution over labels for the given + featureset. + :rtype: ProbDistI + """ + if overridden(self.prob_classify_many): + return self.prob_classify_many([featureset])[0] + else: + raise NotImplementedError() + + def classify_many(self, featuresets): + """ + Apply ``self.classify()`` to each element of ``featuresets``. I.e.: + + return [self.classify(fs) for fs in featuresets] + + :rtype: list(label) + """ + return [self.classify(fs) for fs in featuresets] + + def prob_classify_many(self, featuresets): + """ + Apply ``self.prob_classify()`` to each element of ``featuresets``. I.e.: + + return [self.prob_classify(fs) for fs in featuresets] + + :rtype: list(ProbDistI) + """ + return [self.prob_classify(fs) for fs in featuresets] + + +class MultiClassifierI: + """ + A processing interface for labeling tokens with zero or more + category labels (or "labels"). Labels are typically strs + or ints, but can be any immutable type. The set of labels + that the multi-classifier chooses from must be fixed and finite. + + Subclasses must define: + - ``labels()`` + - either ``classify()`` or ``classify_many()`` (or both) + + Subclasses may define: + - either ``prob_classify()`` or ``prob_classify_many()`` (or both) + """ + + def labels(self): + """ + :return: the list of category labels used by this classifier. + :rtype: list of (immutable) + """ + raise NotImplementedError() + + def classify(self, featureset): + """ + :return: the most appropriate set of labels for the given featureset. + :rtype: set(label) + """ + if overridden(self.classify_many): + return self.classify_many([featureset])[0] + else: + raise NotImplementedError() + + def prob_classify(self, featureset): + """ + :return: a probability distribution over sets of labels for the + given featureset. + :rtype: ProbDistI + """ + if overridden(self.prob_classify_many): + return self.prob_classify_many([featureset])[0] + else: + raise NotImplementedError() + + def classify_many(self, featuresets): + """ + Apply ``self.classify()`` to each element of ``featuresets``. I.e.: + + return [self.classify(fs) for fs in featuresets] + + :rtype: list(set(label)) + """ + return [self.classify(fs) for fs in featuresets] + + def prob_classify_many(self, featuresets): + """ + Apply ``self.prob_classify()`` to each element of ``featuresets``. I.e.: + + return [self.prob_classify(fs) for fs in featuresets] + + :rtype: list(ProbDistI) + """ + return [self.prob_classify(fs) for fs in featuresets] + + +# # [XX] IN PROGRESS: +# class SequenceClassifierI: +# """ +# A processing interface for labeling sequences of tokens with a +# single category label (or "class"). Labels are typically +# strs or ints, but can be any immutable type. The set +# of labels that the classifier chooses from must be fixed and +# finite. +# """ +# def labels(self): +# """ +# :return: the list of category labels used by this classifier. +# :rtype: list of (immutable) +# """ +# raise NotImplementedError() + +# def prob_classify(self, featureset): +# """ +# Return a probability distribution over labels for the given +# featureset. + +# If ``featureset`` is a list of featuresets, then return a +# corresponding list containing the probability distribution +# over labels for each of the given featuresets, where the +# *i*\ th element of this list is the most appropriate label for +# the *i*\ th element of ``featuresets``. +# """ +# raise NotImplementedError() + +# def classify(self, featureset): +# """ +# Return the most appropriate label for the given featureset. + +# If ``featureset`` is a list of featuresets, then return a +# corresponding list containing the most appropriate label for +# each of the given featuresets, where the *i*\ th element of +# this list is the most appropriate label for the *i*\ th element +# of ``featuresets``. +# """ +# raise NotImplementedError() diff --git a/nltk/classify/decisiontree.py b/nltk/classify/decisiontree.py new file mode 100644 index 0000000..d6e12f7 --- /dev/null +++ b/nltk/classify/decisiontree.py @@ -0,0 +1,349 @@ +# Natural Language Toolkit: Decision Tree Classifiers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A classifier model that decides which label to assign to a token on +the basis of a tree structure, where branches correspond to conditions +on feature values, and leaves correspond to label assignments. +""" + +from collections import defaultdict + +from nltk.classify.api import ClassifierI +from nltk.probability import FreqDist, MLEProbDist, entropy + + +class DecisionTreeClassifier(ClassifierI): + def __init__(self, label, feature_name=None, decisions=None, default=None): + """ + :param label: The most likely label for tokens that reach + this node in the decision tree. If this decision tree + has no children, then this label will be assigned to + any token that reaches this decision tree. + :param feature_name: The name of the feature that this + decision tree selects for. + :param decisions: A dictionary mapping from feature values + for the feature identified by ``feature_name`` to + child decision trees. + :param default: The child that will be used if the value of + feature ``feature_name`` does not match any of the keys in + ``decisions``. This is used when constructing binary + decision trees. + """ + self._label = label + self._fname = feature_name + self._decisions = decisions + self._default = default + + def labels(self): + labels = [self._label] + if self._decisions is not None: + for dt in self._decisions.values(): + labels.extend(dt.labels()) + if self._default is not None: + labels.extend(self._default.labels()) + return list(set(labels)) + + def classify(self, featureset): + # Decision leaf: + if self._fname is None: + return self._label + + # Decision tree: + fval = featureset.get(self._fname) + if fval in self._decisions: + return self._decisions[fval].classify(featureset) + elif self._default is not None: + return self._default.classify(featureset) + else: + return self._label + + def error(self, labeled_featuresets): + errors = 0 + for featureset, label in labeled_featuresets: + if self.classify(featureset) != label: + errors += 1 + return errors / len(labeled_featuresets) + + def pretty_format(self, width=70, prefix="", depth=4): + """ + Return a string containing a pretty-printed version of this + decision tree. Each line in this string corresponds to a + single decision tree node or leaf, and indentation is used to + display the structure of the decision tree. + """ + # [xx] display default!! + if self._fname is None: + n = width - len(prefix) - 15 + return "{}{} {}\n".format(prefix, "." * n, self._label) + s = "" + for i, (fval, result) in enumerate( + sorted( + self._decisions.items(), + key=lambda item: (item[0] in [None, False, True], str(item[0]).lower()), + ) + ): + hdr = f"{prefix}{self._fname}={fval}? " + n = width - 15 - len(hdr) + s += "{}{} {}\n".format(hdr, "." * (n), result._label) + if result._fname is not None and depth > 1: + s += result.pretty_format(width, prefix + " ", depth - 1) + if self._default is not None: + n = width - len(prefix) - 21 + s += "{}else: {} {}\n".format(prefix, "." * n, self._default._label) + if self._default._fname is not None and depth > 1: + s += self._default.pretty_format(width, prefix + " ", depth - 1) + return s + + def pseudocode(self, prefix="", depth=4): + """ + Return a string representation of this decision tree that + expresses the decisions it makes as a nested set of pseudocode + if statements. + """ + if self._fname is None: + return f"{prefix}return {self._label!r}\n" + s = "" + for fval, result in sorted( + self._decisions.items(), + key=lambda item: (item[0] in [None, False, True], str(item[0]).lower()), + ): + s += f"{prefix}if {self._fname} == {fval!r}: " + if result._fname is not None and depth > 1: + s += "\n" + result.pseudocode(prefix + " ", depth - 1) + else: + s += f"return {result._label!r}\n" + if self._default is not None: + if len(self._decisions) == 1: + s += "{}if {} != {!r}: ".format( + prefix, self._fname, list(self._decisions.keys())[0] + ) + else: + s += f"{prefix}else: " + if self._default._fname is not None and depth > 1: + s += "\n" + self._default.pseudocode(prefix + " ", depth - 1) + else: + s += f"return {self._default._label!r}\n" + return s + + def __str__(self): + return self.pretty_format() + + @staticmethod + def train( + labeled_featuresets, + entropy_cutoff=0.05, + depth_cutoff=100, + support_cutoff=10, + binary=False, + feature_values=None, + verbose=False, + ): + """ + :param binary: If true, then treat all feature/value pairs as + individual binary features, rather than using a single n-way + branch for each feature. + """ + # Collect a list of all feature names. + feature_names = set() + for featureset, label in labeled_featuresets: + for fname in featureset: + feature_names.add(fname) + + # Collect a list of the values each feature can take. + if feature_values is None and binary: + feature_values = defaultdict(set) + for featureset, label in labeled_featuresets: + for fname, fval in featureset.items(): + feature_values[fname].add(fval) + + # Start with a stump. + if not binary: + tree = DecisionTreeClassifier.best_stump( + feature_names, labeled_featuresets, verbose + ) + else: + tree = DecisionTreeClassifier.best_binary_stump( + feature_names, labeled_featuresets, feature_values, verbose + ) + + # Refine the stump. + tree.refine( + labeled_featuresets, + entropy_cutoff, + depth_cutoff - 1, + support_cutoff, + binary, + feature_values, + verbose, + ) + + # Return it + return tree + + @staticmethod + def leaf(labeled_featuresets): + label = FreqDist(label for (featureset, label) in labeled_featuresets).max() + return DecisionTreeClassifier(label) + + @staticmethod + def stump(feature_name, labeled_featuresets): + label = FreqDist(label for (featureset, label) in labeled_featuresets).max() + + # Find the best label for each value. + freqs = defaultdict(FreqDist) # freq(label|value) + for featureset, label in labeled_featuresets: + feature_value = featureset.get(feature_name) + freqs[feature_value][label] += 1 + + decisions = {val: DecisionTreeClassifier(freqs[val].max()) for val in freqs} + return DecisionTreeClassifier(label, feature_name, decisions) + + def refine( + self, + labeled_featuresets, + entropy_cutoff, + depth_cutoff, + support_cutoff, + binary=False, + feature_values=None, + verbose=False, + ): + if len(labeled_featuresets) <= support_cutoff: + return + if self._fname is None: + return + if depth_cutoff <= 0: + return + for fval in self._decisions: + fval_featuresets = [ + (featureset, label) + for (featureset, label) in labeled_featuresets + if featureset.get(self._fname) == fval + ] + + label_freqs = FreqDist(label for (featureset, label) in fval_featuresets) + if entropy(MLEProbDist(label_freqs)) > entropy_cutoff: + self._decisions[fval] = DecisionTreeClassifier.train( + fval_featuresets, + entropy_cutoff, + depth_cutoff, + support_cutoff, + binary, + feature_values, + verbose, + ) + if self._default is not None: + default_featuresets = [ + (featureset, label) + for (featureset, label) in labeled_featuresets + if featureset.get(self._fname) not in self._decisions + ] + label_freqs = FreqDist(label for (featureset, label) in default_featuresets) + if entropy(MLEProbDist(label_freqs)) > entropy_cutoff: + self._default = DecisionTreeClassifier.train( + default_featuresets, + entropy_cutoff, + depth_cutoff, + support_cutoff, + binary, + feature_values, + verbose, + ) + + @staticmethod + def best_stump(feature_names, labeled_featuresets, verbose=False): + best_stump = DecisionTreeClassifier.leaf(labeled_featuresets) + best_error = best_stump.error(labeled_featuresets) + for fname in feature_names: + stump = DecisionTreeClassifier.stump(fname, labeled_featuresets) + stump_error = stump.error(labeled_featuresets) + if stump_error < best_error: + best_error = stump_error + best_stump = stump + if verbose: + print( + "best stump for {:6d} toks uses {:20} err={:6.4f}".format( + len(labeled_featuresets), best_stump._fname, best_error + ) + ) + return best_stump + + @staticmethod + def binary_stump(feature_name, feature_value, labeled_featuresets): + label = FreqDist(label for (featureset, label) in labeled_featuresets).max() + + # Find the best label for each value. + pos_fdist = FreqDist() + neg_fdist = FreqDist() + for featureset, label in labeled_featuresets: + if featureset.get(feature_name) == feature_value: + pos_fdist[label] += 1 + else: + neg_fdist[label] += 1 + + decisions = {} + default = label + # But hopefully we have observations! + if pos_fdist.N() > 0: + decisions = {feature_value: DecisionTreeClassifier(pos_fdist.max())} + if neg_fdist.N() > 0: + default = DecisionTreeClassifier(neg_fdist.max()) + + return DecisionTreeClassifier(label, feature_name, decisions, default) + + @staticmethod + def best_binary_stump( + feature_names, labeled_featuresets, feature_values, verbose=False + ): + best_stump = DecisionTreeClassifier.leaf(labeled_featuresets) + best_error = best_stump.error(labeled_featuresets) + for fname in feature_names: + for fval in feature_values[fname]: + stump = DecisionTreeClassifier.binary_stump( + fname, fval, labeled_featuresets + ) + stump_error = stump.error(labeled_featuresets) + if stump_error < best_error: + best_error = stump_error + best_stump = stump + if verbose: + if best_stump._decisions: + descr = "{}={}".format( + best_stump._fname, list(best_stump._decisions.keys())[0] + ) + else: + descr = "(default)" + print( + "best stump for {:6d} toks uses {:20} err={:6.4f}".format( + len(labeled_featuresets), descr, best_error + ) + ) + return best_stump + + +##////////////////////////////////////////////////////// +## Demo +##////////////////////////////////////////////////////// + + +def f(x): + return DecisionTreeClassifier.train(x, binary=True, verbose=True) + + +def demo(): + from nltk.classify.util import binary_names_demo_features, names_demo + + classifier = names_demo( + f, binary_names_demo_features # DecisionTreeClassifier.train, + ) + print(classifier.pretty_format(depth=7)) + print(classifier.pseudocode(depth=7)) + + +if __name__ == "__main__": + demo() diff --git a/nltk/classify/maxent.py b/nltk/classify/maxent.py new file mode 100644 index 0000000..73ff4b0 --- /dev/null +++ b/nltk/classify/maxent.py @@ -0,0 +1,1628 @@ +# Natural Language Toolkit: Maximum Entropy Classifiers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Dmitry Chichkov (TypedMaxentFeatureEncoding) +# URL: +# For license information, see LICENSE.TXT + +""" +A classifier model based on maximum entropy modeling framework. This +framework considers all of the probability distributions that are +empirically consistent with the training data; and chooses the +distribution with the highest entropy. A probability distribution is +"empirically consistent" with a set of training data if its estimated +frequency with which a class and a feature vector value co-occur is +equal to the actual frequency in the data. + +Terminology: 'feature' +====================== +The term *feature* is usually used to refer to some property of an +unlabeled token. For example, when performing word sense +disambiguation, we might define a ``'prevword'`` feature whose value is +the word preceding the target word. However, in the context of +maxent modeling, the term *feature* is typically used to refer to a +property of a "labeled" token. In order to prevent confusion, we +will introduce two distinct terms to disambiguate these two different +concepts: + + - An "input-feature" is a property of an unlabeled token. + - A "joint-feature" is a property of a labeled token. + +In the rest of the ``nltk.classify`` module, the term "features" is +used to refer to what we will call "input-features" in this module. + +In literature that describes and discusses maximum entropy models, +input-features are typically called "contexts", and joint-features +are simply referred to as "features". + +Converting Input-Features to Joint-Features +------------------------------------------- +In maximum entropy models, joint-features are required to have numeric +values. Typically, each input-feature ``input_feat`` is mapped to a +set of joint-features of the form: + +| joint_feat(token, label) = { 1 if input_feat(token) == feat_val +| { and label == some_label +| { +| { 0 otherwise + +For all values of ``feat_val`` and ``some_label``. This mapping is +performed by classes that implement the ``MaxentFeatureEncodingI`` +interface. +""" +try: + import numpy +except ImportError: + pass + +import os +import tempfile +from collections import defaultdict + +from nltk.classify.api import ClassifierI +from nltk.classify.megam import call_megam, parse_megam_weights, write_megam_file +from nltk.classify.tadm import call_tadm, parse_tadm_weights, write_tadm_file +from nltk.classify.util import CutoffChecker, accuracy, log_likelihood +from nltk.data import gzip_open_unicode +from nltk.probability import DictionaryProbDist +from nltk.util import OrderedDict + +__docformat__ = "epytext en" + +###################################################################### +# { Classifier Model +###################################################################### + + +class MaxentClassifier(ClassifierI): + """ + A maximum entropy classifier (also known as a "conditional + exponential classifier"). This classifier is parameterized by a + set of "weights", which are used to combine the joint-features + that are generated from a featureset by an "encoding". In + particular, the encoding maps each ``(featureset, label)`` pair to + a vector. The probability of each label is then computed using + the following equation:: + + dotprod(weights, encode(fs,label)) + prob(fs|label) = --------------------------------------------------- + sum(dotprod(weights, encode(fs,l)) for l in labels) + + Where ``dotprod`` is the dot product:: + + dotprod(a,b) = sum(x*y for (x,y) in zip(a,b)) + """ + + def __init__(self, encoding, weights, logarithmic=True): + """ + Construct a new maxent classifier model. Typically, new + classifier models are created using the ``train()`` method. + + :type encoding: MaxentFeatureEncodingI + :param encoding: An encoding that is used to convert the + featuresets that are given to the ``classify`` method into + joint-feature vectors, which are used by the maxent + classifier model. + + :type weights: list of float + :param weights: The feature weight vector for this classifier. + + :type logarithmic: bool + :param logarithmic: If false, then use non-logarithmic weights. + """ + self._encoding = encoding + self._weights = weights + self._logarithmic = logarithmic + # self._logarithmic = False + assert encoding.length() == len(weights) + + def labels(self): + return self._encoding.labels() + + def set_weights(self, new_weights): + """ + Set the feature weight vector for this classifier. + :param new_weights: The new feature weight vector. + :type new_weights: list of float + """ + self._weights = new_weights + assert self._encoding.length() == len(new_weights) + + def weights(self): + """ + :return: The feature weight vector for this classifier. + :rtype: list of float + """ + return self._weights + + def classify(self, featureset): + return self.prob_classify(featureset).max() + + def prob_classify(self, featureset): + prob_dict = {} + for label in self._encoding.labels(): + feature_vector = self._encoding.encode(featureset, label) + + if self._logarithmic: + total = 0.0 + for f_id, f_val in feature_vector: + total += self._weights[f_id] * f_val + prob_dict[label] = total + + else: + prod = 1.0 + for f_id, f_val in feature_vector: + prod *= self._weights[f_id] ** f_val + prob_dict[label] = prod + + # Normalize the dictionary to give a probability distribution + return DictionaryProbDist(prob_dict, log=self._logarithmic, normalize=True) + + def explain(self, featureset, columns=4): + """ + Print a table showing the effect of each of the features in + the given feature set, and how they combine to determine the + probabilities of each label for that featureset. + """ + descr_width = 50 + TEMPLATE = " %-" + str(descr_width - 2) + "s%s%8.3f" + + pdist = self.prob_classify(featureset) + labels = sorted(pdist.samples(), key=pdist.prob, reverse=True) + labels = labels[:columns] + print( + " Feature".ljust(descr_width) + + "".join("%8s" % (("%s" % l)[:7]) for l in labels) + ) + print(" " + "-" * (descr_width - 2 + 8 * len(labels))) + sums = defaultdict(int) + for i, label in enumerate(labels): + feature_vector = self._encoding.encode(featureset, label) + feature_vector.sort( + key=lambda fid__: abs(self._weights[fid__[0]]), reverse=True + ) + for f_id, f_val in feature_vector: + if self._logarithmic: + score = self._weights[f_id] * f_val + else: + score = self._weights[f_id] ** f_val + descr = self._encoding.describe(f_id) + descr = descr.split(" and label is ")[0] # hack + descr += " (%s)" % f_val # hack + if len(descr) > 47: + descr = descr[:44] + "..." + print(TEMPLATE % (descr, i * 8 * " ", score)) + sums[label] += score + print(" " + "-" * (descr_width - 1 + 8 * len(labels))) + print( + " TOTAL:".ljust(descr_width) + "".join("%8.3f" % sums[l] for l in labels) + ) + print( + " PROBS:".ljust(descr_width) + + "".join("%8.3f" % pdist.prob(l) for l in labels) + ) + + def most_informative_features(self, n=10): + """ + Generates the ranked list of informative features from most to least. + """ + if hasattr(self, "_most_informative_features"): + return self._most_informative_features[:n] + else: + self._most_informative_features = sorted( + list(range(len(self._weights))), + key=lambda fid: abs(self._weights[fid]), + reverse=True, + ) + return self._most_informative_features[:n] + + def show_most_informative_features(self, n=10, show="all"): + """ + :param show: all, neg, or pos (for negative-only or positive-only) + :type show: str + :param n: The no. of top features + :type n: int + """ + # Use None the full list of ranked features. + fids = self.most_informative_features(None) + if show == "pos": + fids = [fid for fid in fids if self._weights[fid] > 0] + elif show == "neg": + fids = [fid for fid in fids if self._weights[fid] < 0] + for fid in fids[:n]: + print(f"{self._weights[fid]:8.3f} {self._encoding.describe(fid)}") + + def __repr__(self): + return "" % ( + len(self._encoding.labels()), + self._encoding.length(), + ) + + #: A list of the algorithm names that are accepted for the + #: ``train()`` method's ``algorithm`` parameter. + ALGORITHMS = ["GIS", "IIS", "MEGAM", "TADM"] + + @classmethod + def train( + cls, + train_toks, + algorithm=None, + trace=3, + encoding=None, + labels=None, + gaussian_prior_sigma=0, + **cutoffs, + ): + """ + Train a new maxent classifier based on the given corpus of + training samples. This classifier will have its weights + chosen to maximize entropy while remaining empirically + consistent with the training corpus. + + :rtype: MaxentClassifier + :return: The new maxent classifier + + :type train_toks: list + :param train_toks: Training data, represented as a list of + pairs, the first member of which is a featureset, + and the second of which is a classification label. + + :type algorithm: str + :param algorithm: A case-insensitive string, specifying which + algorithm should be used to train the classifier. The + following algorithms are currently available. + + - Iterative Scaling Methods: Generalized Iterative Scaling (``'GIS'``), + Improved Iterative Scaling (``'IIS'``) + - External Libraries (requiring megam): + LM-BFGS algorithm, with training performed by Megam (``'megam'``) + + The default algorithm is ``'IIS'``. + + :type trace: int + :param trace: The level of diagnostic tracing output to produce. + Higher values produce more verbose output. + :type encoding: MaxentFeatureEncodingI + :param encoding: A feature encoding, used to convert featuresets + into feature vectors. If none is specified, then a + ``BinaryMaxentFeatureEncoding`` will be built based on the + features that are attested in the training corpus. + :type labels: list(str) + :param labels: The set of possible labels. If none is given, then + the set of all labels attested in the training data will be + used instead. + :param gaussian_prior_sigma: The sigma value for a gaussian + prior on model weights. Currently, this is supported by + ``megam``. For other algorithms, its value is ignored. + :param cutoffs: Arguments specifying various conditions under + which the training should be halted. (Some of the cutoff + conditions are not supported by some algorithms.) + + - ``max_iter=v``: Terminate after ``v`` iterations. + - ``min_ll=v``: Terminate after the negative average + log-likelihood drops under ``v``. + - ``min_lldelta=v``: Terminate if a single iteration improves + log likelihood by less than ``v``. + """ + if algorithm is None: + algorithm = "iis" + for key in cutoffs: + if key not in ( + "max_iter", + "min_ll", + "min_lldelta", + "max_acc", + "min_accdelta", + "count_cutoff", + "norm", + "explicit", + "bernoulli", + ): + raise TypeError("Unexpected keyword arg %r" % key) + algorithm = algorithm.lower() + if algorithm == "iis": + return train_maxent_classifier_with_iis( + train_toks, trace, encoding, labels, **cutoffs + ) + elif algorithm == "gis": + return train_maxent_classifier_with_gis( + train_toks, trace, encoding, labels, **cutoffs + ) + elif algorithm == "megam": + return train_maxent_classifier_with_megam( + train_toks, trace, encoding, labels, gaussian_prior_sigma, **cutoffs + ) + elif algorithm == "tadm": + kwargs = cutoffs + kwargs["trace"] = trace + kwargs["encoding"] = encoding + kwargs["labels"] = labels + kwargs["gaussian_prior_sigma"] = gaussian_prior_sigma + return TadmMaxentClassifier.train(train_toks, **kwargs) + else: + raise ValueError("Unknown algorithm %s" % algorithm) + + +#: Alias for MaxentClassifier. +ConditionalExponentialClassifier = MaxentClassifier + + +###################################################################### +# { Feature Encodings +###################################################################### + + +class MaxentFeatureEncodingI: + """ + A mapping that converts a set of input-feature values to a vector + of joint-feature values, given a label. This conversion is + necessary to translate featuresets into a format that can be used + by maximum entropy models. + + The set of joint-features used by a given encoding is fixed, and + each index in the generated joint-feature vectors corresponds to a + single joint-feature. The length of the generated joint-feature + vectors is therefore constant (for a given encoding). + + Because the joint-feature vectors generated by + ``MaxentFeatureEncodingI`` are typically very sparse, they are + represented as a list of ``(index, value)`` tuples, specifying the + value of each non-zero joint-feature. + + Feature encodings are generally created using the ``train()`` + method, which generates an appropriate encoding based on the + input-feature values and labels that are present in a given + corpus. + """ + + def encode(self, featureset, label): + """ + Given a (featureset, label) pair, return the corresponding + vector of joint-feature values. This vector is represented as + a list of ``(index, value)`` tuples, specifying the value of + each non-zero joint-feature. + + :type featureset: dict + :rtype: list(tuple(int, int)) + """ + raise NotImplementedError() + + def length(self): + """ + :return: The size of the fixed-length joint-feature vectors + that are generated by this encoding. + :rtype: int + """ + raise NotImplementedError() + + def labels(self): + """ + :return: A list of the \"known labels\" -- i.e., all labels + ``l`` such that ``self.encode(fs,l)`` can be a nonzero + joint-feature vector for some value of ``fs``. + :rtype: list + """ + raise NotImplementedError() + + def describe(self, fid): + """ + :return: A string describing the value of the joint-feature + whose index in the generated feature vectors is ``fid``. + :rtype: str + """ + raise NotImplementedError() + + def train(cls, train_toks): + """ + Construct and return new feature encoding, based on a given + training corpus ``train_toks``. + + :type train_toks: list(tuple(dict, str)) + :param train_toks: Training data, represented as a list of + pairs, the first member of which is a feature dictionary, + and the second of which is a classification label. + """ + raise NotImplementedError() + + +class FunctionBackedMaxentFeatureEncoding(MaxentFeatureEncodingI): + """ + A feature encoding that calls a user-supplied function to map a + given featureset/label pair to a sparse joint-feature vector. + """ + + def __init__(self, func, length, labels): + """ + Construct a new feature encoding based on the given function. + + :type func: (callable) + :param func: A function that takes two arguments, a featureset + and a label, and returns the sparse joint feature vector + that encodes them:: + + func(featureset, label) -> feature_vector + + This sparse joint feature vector (``feature_vector``) is a + list of ``(index,value)`` tuples. + + :type length: int + :param length: The size of the fixed-length joint-feature + vectors that are generated by this encoding. + + :type labels: list + :param labels: A list of the \"known labels\" for this + encoding -- i.e., all labels ``l`` such that + ``self.encode(fs,l)`` can be a nonzero joint-feature vector + for some value of ``fs``. + """ + self._length = length + self._func = func + self._labels = labels + + def encode(self, featureset, label): + return self._func(featureset, label) + + def length(self): + return self._length + + def labels(self): + return self._labels + + def describe(self, fid): + return "no description available" + + +class BinaryMaxentFeatureEncoding(MaxentFeatureEncodingI): + """ + A feature encoding that generates vectors containing a binary + joint-features of the form: + + | joint_feat(fs, l) = { 1 if (fs[fname] == fval) and (l == label) + | { + | { 0 otherwise + + Where ``fname`` is the name of an input-feature, ``fval`` is a value + for that input-feature, and ``label`` is a label. + + Typically, these features are constructed based on a training + corpus, using the ``train()`` method. This method will create one + feature for each combination of ``fname``, ``fval``, and ``label`` + that occurs at least once in the training corpus. + + The ``unseen_features`` parameter can be used to add "unseen-value + features", which are used whenever an input feature has a value + that was not encountered in the training corpus. These features + have the form: + + | joint_feat(fs, l) = { 1 if is_unseen(fname, fs[fname]) + | { and l == label + | { + | { 0 otherwise + + Where ``is_unseen(fname, fval)`` is true if the encoding does not + contain any joint features that are true when ``fs[fname]==fval``. + + The ``alwayson_features`` parameter can be used to add "always-on + features", which have the form:: + + | joint_feat(fs, l) = { 1 if (l == label) + | { + | { 0 otherwise + + These always-on features allow the maxent model to directly model + the prior probabilities of each label. + """ + + def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False): + """ + :param labels: A list of the \"known labels\" for this encoding. + + :param mapping: A dictionary mapping from ``(fname,fval,label)`` + tuples to corresponding joint-feature indexes. These + indexes must be the set of integers from 0...len(mapping). + If ``mapping[fname,fval,label]=id``, then + ``self.encode(..., fname:fval, ..., label)[id]`` is 1; + otherwise, it is 0. + + :param unseen_features: If true, then include unseen value + features in the generated joint-feature vectors. + + :param alwayson_features: If true, then include always-on + features in the generated joint-feature vectors. + """ + if set(mapping.values()) != set(range(len(mapping))): + raise ValueError( + "Mapping values must be exactly the " + "set of integers from 0...len(mapping)" + ) + + self._labels = list(labels) + """A list of attested labels.""" + + self._mapping = mapping + """dict mapping from (fname,fval,label) -> fid""" + + self._length = len(mapping) + """The length of generated joint feature vectors.""" + + self._alwayson = None + """dict mapping from label -> fid""" + + self._unseen = None + """dict mapping from fname -> fid""" + + if alwayson_features: + self._alwayson = { + label: i + self._length for (i, label) in enumerate(labels) + } + self._length += len(self._alwayson) + + if unseen_features: + fnames = {fname for (fname, fval, label) in mapping} + self._unseen = {fname: i + self._length for (i, fname) in enumerate(fnames)} + self._length += len(fnames) + + def encode(self, featureset, label): + # Inherit docs. + encoding = [] + + # Convert input-features to joint-features: + for fname, fval in featureset.items(): + # Known feature name & value: + if (fname, fval, label) in self._mapping: + encoding.append((self._mapping[fname, fval, label], 1)) + + # Otherwise, we might want to fire an "unseen-value feature". + elif self._unseen: + # Have we seen this fname/fval combination with any label? + for label2 in self._labels: + if (fname, fval, label2) in self._mapping: + break # we've seen this fname/fval combo + # We haven't -- fire the unseen-value feature + else: + if fname in self._unseen: + encoding.append((self._unseen[fname], 1)) + + # Add always-on features: + if self._alwayson and label in self._alwayson: + encoding.append((self._alwayson[label], 1)) + + return encoding + + def describe(self, f_id): + # Inherit docs. + if not isinstance(f_id, int): + raise TypeError("describe() expected an int") + try: + self._inv_mapping + except AttributeError: + self._inv_mapping = [-1] * len(self._mapping) + for info, i in self._mapping.items(): + self._inv_mapping[i] = info + + if f_id < len(self._mapping): + (fname, fval, label) = self._inv_mapping[f_id] + return f"{fname}=={fval!r} and label is {label!r}" + elif self._alwayson and f_id in self._alwayson.values(): + for label, f_id2 in self._alwayson.items(): + if f_id == f_id2: + return "label is %r" % label + elif self._unseen and f_id in self._unseen.values(): + for fname, f_id2 in self._unseen.items(): + if f_id == f_id2: + return "%s is unseen" % fname + else: + raise ValueError("Bad feature id") + + def labels(self): + # Inherit docs. + return self._labels + + def length(self): + # Inherit docs. + return self._length + + @classmethod + def train(cls, train_toks, count_cutoff=0, labels=None, **options): + """ + Construct and return new feature encoding, based on a given + training corpus ``train_toks``. See the class description + ``BinaryMaxentFeatureEncoding`` for a description of the + joint-features that will be included in this encoding. + + :type train_toks: list(tuple(dict, str)) + :param train_toks: Training data, represented as a list of + pairs, the first member of which is a feature dictionary, + and the second of which is a classification label. + + :type count_cutoff: int + :param count_cutoff: A cutoff value that is used to discard + rare joint-features. If a joint-feature's value is 1 + fewer than ``count_cutoff`` times in the training corpus, + then that joint-feature is not included in the generated + encoding. + + :type labels: list + :param labels: A list of labels that should be used by the + classifier. If not specified, then the set of labels + attested in ``train_toks`` will be used. + + :param options: Extra parameters for the constructor, such as + ``unseen_features`` and ``alwayson_features``. + """ + mapping = {} # maps (fname, fval, label) -> fid + seen_labels = set() # The set of labels we've encountered + count = defaultdict(int) # maps (fname, fval) -> count + + for tok, label in train_toks: + if labels and label not in labels: + raise ValueError("Unexpected label %s" % label) + seen_labels.add(label) + + # Record each of the features. + for fname, fval in tok.items(): + # If a count cutoff is given, then only add a joint + # feature once the corresponding (fname, fval, label) + # tuple exceeds that cutoff. + count[fname, fval] += 1 + if count[fname, fval] >= count_cutoff: + if (fname, fval, label) not in mapping: + mapping[fname, fval, label] = len(mapping) + + if labels is None: + labels = seen_labels + return cls(labels, mapping, **options) + + +class GISEncoding(BinaryMaxentFeatureEncoding): + """ + A binary feature encoding which adds one new joint-feature to the + joint-features defined by ``BinaryMaxentFeatureEncoding``: a + correction feature, whose value is chosen to ensure that the + sparse vector always sums to a constant non-negative number. This + new feature is used to ensure two preconditions for the GIS + training algorithm: + + - At least one feature vector index must be nonzero for every + token. + - The feature vector must sum to a constant non-negative number + for every token. + """ + + def __init__( + self, labels, mapping, unseen_features=False, alwayson_features=False, C=None + ): + """ + :param C: The correction constant. The value of the correction + feature is based on this value. In particular, its value is + ``C - sum([v for (f,v) in encoding])``. + :seealso: ``BinaryMaxentFeatureEncoding.__init__`` + """ + BinaryMaxentFeatureEncoding.__init__( + self, labels, mapping, unseen_features, alwayson_features + ) + if C is None: + C = len({fname for (fname, fval, label) in mapping}) + 1 + self._C = C + + @property + def C(self): + """The non-negative constant that all encoded feature vectors + will sum to.""" + return self._C + + def encode(self, featureset, label): + # Get the basic encoding. + encoding = BinaryMaxentFeatureEncoding.encode(self, featureset, label) + base_length = BinaryMaxentFeatureEncoding.length(self) + + # Add a correction feature. + total = sum(v for (f, v) in encoding) + if total >= self._C: + raise ValueError("Correction feature is not high enough!") + encoding.append((base_length, self._C - total)) + + # Return the result + return encoding + + def length(self): + return BinaryMaxentFeatureEncoding.length(self) + 1 + + def describe(self, f_id): + if f_id == BinaryMaxentFeatureEncoding.length(self): + return "Correction feature (%s)" % self._C + else: + return BinaryMaxentFeatureEncoding.describe(self, f_id) + + +class TadmEventMaxentFeatureEncoding(BinaryMaxentFeatureEncoding): + def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False): + self._mapping = OrderedDict(mapping) + self._label_mapping = OrderedDict() + BinaryMaxentFeatureEncoding.__init__( + self, labels, self._mapping, unseen_features, alwayson_features + ) + + def encode(self, featureset, label): + encoding = [] + for feature, value in featureset.items(): + if (feature, label) not in self._mapping: + self._mapping[(feature, label)] = len(self._mapping) + if value not in self._label_mapping: + if not isinstance(value, int): + self._label_mapping[value] = len(self._label_mapping) + else: + self._label_mapping[value] = value + encoding.append( + (self._mapping[(feature, label)], self._label_mapping[value]) + ) + return encoding + + def labels(self): + return self._labels + + def describe(self, fid): + for feature, label in self._mapping: + if self._mapping[(feature, label)] == fid: + return (feature, label) + + def length(self): + return len(self._mapping) + + @classmethod + def train(cls, train_toks, count_cutoff=0, labels=None, **options): + mapping = OrderedDict() + if not labels: + labels = [] + + # This gets read twice, so compute the values in case it's lazy. + train_toks = list(train_toks) + + for featureset, label in train_toks: + if label not in labels: + labels.append(label) + + for featureset, label in train_toks: + for label in labels: + for feature in featureset: + if (feature, label) not in mapping: + mapping[(feature, label)] = len(mapping) + + return cls(labels, mapping, **options) + + +class TypedMaxentFeatureEncoding(MaxentFeatureEncodingI): + """ + A feature encoding that generates vectors containing integer, + float and binary joint-features of the form: + + Binary (for string and boolean features): + + | joint_feat(fs, l) = { 1 if (fs[fname] == fval) and (l == label) + | { + | { 0 otherwise + + Value (for integer and float features): + + | joint_feat(fs, l) = { fval if (fs[fname] == type(fval)) + | { and (l == label) + | { + | { not encoded otherwise + + Where ``fname`` is the name of an input-feature, ``fval`` is a value + for that input-feature, and ``label`` is a label. + + Typically, these features are constructed based on a training + corpus, using the ``train()`` method. + + For string and boolean features [type(fval) not in (int, float)] + this method will create one feature for each combination of + ``fname``, ``fval``, and ``label`` that occurs at least once in the + training corpus. + + For integer and float features [type(fval) in (int, float)] this + method will create one feature for each combination of ``fname`` + and ``label`` that occurs at least once in the training corpus. + + For binary features the ``unseen_features`` parameter can be used + to add "unseen-value features", which are used whenever an input + feature has a value that was not encountered in the training + corpus. These features have the form: + + | joint_feat(fs, l) = { 1 if is_unseen(fname, fs[fname]) + | { and l == label + | { + | { 0 otherwise + + Where ``is_unseen(fname, fval)`` is true if the encoding does not + contain any joint features that are true when ``fs[fname]==fval``. + + The ``alwayson_features`` parameter can be used to add "always-on + features", which have the form: + + | joint_feat(fs, l) = { 1 if (l == label) + | { + | { 0 otherwise + + These always-on features allow the maxent model to directly model + the prior probabilities of each label. + """ + + def __init__(self, labels, mapping, unseen_features=False, alwayson_features=False): + """ + :param labels: A list of the \"known labels\" for this encoding. + + :param mapping: A dictionary mapping from ``(fname,fval,label)`` + tuples to corresponding joint-feature indexes. These + indexes must be the set of integers from 0...len(mapping). + If ``mapping[fname,fval,label]=id``, then + ``self.encode({..., fname:fval, ...``, label)[id]} is 1; + otherwise, it is 0. + + :param unseen_features: If true, then include unseen value + features in the generated joint-feature vectors. + + :param alwayson_features: If true, then include always-on + features in the generated joint-feature vectors. + """ + if set(mapping.values()) != set(range(len(mapping))): + raise ValueError( + "Mapping values must be exactly the " + "set of integers from 0...len(mapping)" + ) + + self._labels = list(labels) + """A list of attested labels.""" + + self._mapping = mapping + """dict mapping from (fname,fval,label) -> fid""" + + self._length = len(mapping) + """The length of generated joint feature vectors.""" + + self._alwayson = None + """dict mapping from label -> fid""" + + self._unseen = None + """dict mapping from fname -> fid""" + + if alwayson_features: + self._alwayson = { + label: i + self._length for (i, label) in enumerate(labels) + } + self._length += len(self._alwayson) + + if unseen_features: + fnames = {fname for (fname, fval, label) in mapping} + self._unseen = {fname: i + self._length for (i, fname) in enumerate(fnames)} + self._length += len(fnames) + + def encode(self, featureset, label): + # Inherit docs. + encoding = [] + + # Convert input-features to joint-features: + for fname, fval in featureset.items(): + if isinstance(fval, (int, float)): + # Known feature name & value: + if (fname, type(fval), label) in self._mapping: + encoding.append((self._mapping[fname, type(fval), label], fval)) + else: + # Known feature name & value: + if (fname, fval, label) in self._mapping: + encoding.append((self._mapping[fname, fval, label], 1)) + + # Otherwise, we might want to fire an "unseen-value feature". + elif self._unseen: + # Have we seen this fname/fval combination with any label? + for label2 in self._labels: + if (fname, fval, label2) in self._mapping: + break # we've seen this fname/fval combo + # We haven't -- fire the unseen-value feature + else: + if fname in self._unseen: + encoding.append((self._unseen[fname], 1)) + + # Add always-on features: + if self._alwayson and label in self._alwayson: + encoding.append((self._alwayson[label], 1)) + + return encoding + + def describe(self, f_id): + # Inherit docs. + if not isinstance(f_id, int): + raise TypeError("describe() expected an int") + try: + self._inv_mapping + except AttributeError: + self._inv_mapping = [-1] * len(self._mapping) + for info, i in self._mapping.items(): + self._inv_mapping[i] = info + + if f_id < len(self._mapping): + (fname, fval, label) = self._inv_mapping[f_id] + return f"{fname}=={fval!r} and label is {label!r}" + elif self._alwayson and f_id in self._alwayson.values(): + for label, f_id2 in self._alwayson.items(): + if f_id == f_id2: + return "label is %r" % label + elif self._unseen and f_id in self._unseen.values(): + for fname, f_id2 in self._unseen.items(): + if f_id == f_id2: + return "%s is unseen" % fname + else: + raise ValueError("Bad feature id") + + def labels(self): + # Inherit docs. + return self._labels + + def length(self): + # Inherit docs. + return self._length + + @classmethod + def train(cls, train_toks, count_cutoff=0, labels=None, **options): + """ + Construct and return new feature encoding, based on a given + training corpus ``train_toks``. See the class description + ``TypedMaxentFeatureEncoding`` for a description of the + joint-features that will be included in this encoding. + + Note: recognized feature values types are (int, float), over + types are interpreted as regular binary features. + + :type train_toks: list(tuple(dict, str)) + :param train_toks: Training data, represented as a list of + pairs, the first member of which is a feature dictionary, + and the second of which is a classification label. + + :type count_cutoff: int + :param count_cutoff: A cutoff value that is used to discard + rare joint-features. If a joint-feature's value is 1 + fewer than ``count_cutoff`` times in the training corpus, + then that joint-feature is not included in the generated + encoding. + + :type labels: list + :param labels: A list of labels that should be used by the + classifier. If not specified, then the set of labels + attested in ``train_toks`` will be used. + + :param options: Extra parameters for the constructor, such as + ``unseen_features`` and ``alwayson_features``. + """ + mapping = {} # maps (fname, fval, label) -> fid + seen_labels = set() # The set of labels we've encountered + count = defaultdict(int) # maps (fname, fval) -> count + + for tok, label in train_toks: + if labels and label not in labels: + raise ValueError("Unexpected label %s" % label) + seen_labels.add(label) + + # Record each of the features. + for fname, fval in tok.items(): + if type(fval) in (int, float): + fval = type(fval) + # If a count cutoff is given, then only add a joint + # feature once the corresponding (fname, fval, label) + # tuple exceeds that cutoff. + count[fname, fval] += 1 + if count[fname, fval] >= count_cutoff: + if (fname, fval, label) not in mapping: + mapping[fname, fval, label] = len(mapping) + + if labels is None: + labels = seen_labels + return cls(labels, mapping, **options) + + +###################################################################### +# { Classifier Trainer: Generalized Iterative Scaling +###################################################################### + + +def train_maxent_classifier_with_gis( + train_toks, trace=3, encoding=None, labels=None, **cutoffs +): + """ + Train a new ``ConditionalExponentialClassifier``, using the given + training samples, using the Generalized Iterative Scaling + algorithm. This ``ConditionalExponentialClassifier`` will encode + the model that maximizes entropy from all the models that are + empirically consistent with ``train_toks``. + + :see: ``train_maxent_classifier()`` for parameter descriptions. + """ + cutoffs.setdefault("max_iter", 100) + cutoffchecker = CutoffChecker(cutoffs) + + # Construct an encoding from the training data. + if encoding is None: + encoding = GISEncoding.train(train_toks, labels=labels) + + if not hasattr(encoding, "C"): + raise TypeError( + "The GIS algorithm requires an encoding that " + "defines C (e.g., GISEncoding)." + ) + + # Cinv is the inverse of the sum of each joint feature vector. + # This controls the learning rate: higher Cinv (or lower C) gives + # faster learning. + Cinv = 1.0 / encoding.C + + # Count how many times each feature occurs in the training data. + empirical_fcount = calculate_empirical_fcount(train_toks, encoding) + + # Check for any features that are not attested in train_toks. + unattested = set(numpy.nonzero(empirical_fcount == 0)[0]) + + # Build the classifier. Start with weight=0 for each attested + # feature, and weight=-infinity for each unattested feature. + weights = numpy.zeros(len(empirical_fcount), "d") + for fid in unattested: + weights[fid] = numpy.NINF + classifier = ConditionalExponentialClassifier(encoding, weights) + + # Take the log of the empirical fcount. + log_empirical_fcount = numpy.log2(empirical_fcount) + del empirical_fcount + + if trace > 0: + print(" ==> Training (%d iterations)" % cutoffs["max_iter"]) + if trace > 2: + print() + print(" Iteration Log Likelihood Accuracy") + print(" ---------------------------------------") + + # Train the classifier. + try: + while True: + if trace > 2: + ll = cutoffchecker.ll or log_likelihood(classifier, train_toks) + acc = cutoffchecker.acc or accuracy(classifier, train_toks) + iternum = cutoffchecker.iter + print(" %9d %14.5f %9.3f" % (iternum, ll, acc)) + + # Use the model to estimate the number of times each + # feature should occur in the training data. + estimated_fcount = calculate_estimated_fcount( + classifier, train_toks, encoding + ) + + # Take the log of estimated fcount (avoid taking log(0).) + for fid in unattested: + estimated_fcount[fid] += 1 + log_estimated_fcount = numpy.log2(estimated_fcount) + del estimated_fcount + + # Update the classifier weights + weights = classifier.weights() + weights += (log_empirical_fcount - log_estimated_fcount) * Cinv + classifier.set_weights(weights) + + # Check the log-likelihood & accuracy cutoffs. + if cutoffchecker.check(classifier, train_toks): + break + + except KeyboardInterrupt: + print(" Training stopped: keyboard interrupt") + + if trace > 2: + ll = log_likelihood(classifier, train_toks) + acc = accuracy(classifier, train_toks) + print(f" Final {ll:14.5f} {acc:9.3f}") + + # Return the classifier. + return classifier + + +def calculate_empirical_fcount(train_toks, encoding): + fcount = numpy.zeros(encoding.length(), "d") + + for tok, label in train_toks: + for index, val in encoding.encode(tok, label): + fcount[index] += val + + return fcount + + +def calculate_estimated_fcount(classifier, train_toks, encoding): + fcount = numpy.zeros(encoding.length(), "d") + + for tok, label in train_toks: + pdist = classifier.prob_classify(tok) + for label in pdist.samples(): + prob = pdist.prob(label) + for fid, fval in encoding.encode(tok, label): + fcount[fid] += prob * fval + + return fcount + + +###################################################################### +# { Classifier Trainer: Improved Iterative Scaling +###################################################################### + + +def train_maxent_classifier_with_iis( + train_toks, trace=3, encoding=None, labels=None, **cutoffs +): + """ + Train a new ``ConditionalExponentialClassifier``, using the given + training samples, using the Improved Iterative Scaling algorithm. + This ``ConditionalExponentialClassifier`` will encode the model + that maximizes entropy from all the models that are empirically + consistent with ``train_toks``. + + :see: ``train_maxent_classifier()`` for parameter descriptions. + """ + cutoffs.setdefault("max_iter", 100) + cutoffchecker = CutoffChecker(cutoffs) + + # Construct an encoding from the training data. + if encoding is None: + encoding = BinaryMaxentFeatureEncoding.train(train_toks, labels=labels) + + # Count how many times each feature occurs in the training data. + empirical_ffreq = calculate_empirical_fcount(train_toks, encoding) / len(train_toks) + + # Find the nf map, and related variables nfarray and nfident. + # nf is the sum of the features for a given labeled text. + # nfmap compresses this sparse set of values to a dense list. + # nfarray performs the reverse operation. nfident is + # nfarray multiplied by an identity matrix. + nfmap = calculate_nfmap(train_toks, encoding) + nfarray = numpy.array(sorted(nfmap, key=nfmap.__getitem__), "d") + nftranspose = numpy.reshape(nfarray, (len(nfarray), 1)) + + # Check for any features that are not attested in train_toks. + unattested = set(numpy.nonzero(empirical_ffreq == 0)[0]) + + # Build the classifier. Start with weight=0 for each attested + # feature, and weight=-infinity for each unattested feature. + weights = numpy.zeros(len(empirical_ffreq), "d") + for fid in unattested: + weights[fid] = numpy.NINF + classifier = ConditionalExponentialClassifier(encoding, weights) + + if trace > 0: + print(" ==> Training (%d iterations)" % cutoffs["max_iter"]) + if trace > 2: + print() + print(" Iteration Log Likelihood Accuracy") + print(" ---------------------------------------") + + # Train the classifier. + try: + while True: + if trace > 2: + ll = cutoffchecker.ll or log_likelihood(classifier, train_toks) + acc = cutoffchecker.acc or accuracy(classifier, train_toks) + iternum = cutoffchecker.iter + print(" %9d %14.5f %9.3f" % (iternum, ll, acc)) + + # Calculate the deltas for this iteration, using Newton's method. + deltas = calculate_deltas( + train_toks, + classifier, + unattested, + empirical_ffreq, + nfmap, + nfarray, + nftranspose, + encoding, + ) + + # Use the deltas to update our weights. + weights = classifier.weights() + weights += deltas + classifier.set_weights(weights) + + # Check the log-likelihood & accuracy cutoffs. + if cutoffchecker.check(classifier, train_toks): + break + + except KeyboardInterrupt: + print(" Training stopped: keyboard interrupt") + + if trace > 2: + ll = log_likelihood(classifier, train_toks) + acc = accuracy(classifier, train_toks) + print(f" Final {ll:14.5f} {acc:9.3f}") + + # Return the classifier. + return classifier + + +def calculate_nfmap(train_toks, encoding): + """ + Construct a map that can be used to compress ``nf`` (which is + typically sparse). + + *nf(feature_vector)* is the sum of the feature values for + *feature_vector*. + + This represents the number of features that are active for a + given labeled text. This method finds all values of *nf(t)* + that are attested for at least one token in the given list of + training tokens; and constructs a dictionary mapping these + attested values to a continuous range *0...N*. For example, + if the only values of *nf()* that were attested were 3, 5, and + 7, then ``_nfmap`` might return the dictionary ``{3:0, 5:1, 7:2}``. + + :return: A map that can be used to compress ``nf`` to a dense + vector. + :rtype: dict(int -> int) + """ + # Map from nf to indices. This allows us to use smaller arrays. + nfset = set() + for tok, _ in train_toks: + for label in encoding.labels(): + nfset.add(sum(val for (id, val) in encoding.encode(tok, label))) + return {nf: i for (i, nf) in enumerate(nfset)} + + +def calculate_deltas( + train_toks, + classifier, + unattested, + ffreq_empirical, + nfmap, + nfarray, + nftranspose, + encoding, +): + r""" + Calculate the update values for the classifier weights for + this iteration of IIS. These update weights are the value of + ``delta`` that solves the equation:: + + ffreq_empirical[i] + = + SUM[fs,l] (classifier.prob_classify(fs).prob(l) * + feature_vector(fs,l)[i] * + exp(delta[i] * nf(feature_vector(fs,l)))) + + Where: + - *(fs,l)* is a (featureset, label) tuple from ``train_toks`` + - *feature_vector(fs,l)* = ``encoding.encode(fs,l)`` + - *nf(vector)* = ``sum([val for (id,val) in vector])`` + + This method uses Newton's method to solve this equation for + *delta[i]*. In particular, it starts with a guess of + ``delta[i]`` = 1; and iteratively updates ``delta`` with: + + | delta[i] -= (ffreq_empirical[i] - sum1[i])/(-sum2[i]) + + until convergence, where *sum1* and *sum2* are defined as: + + | sum1[i](delta) = SUM[fs,l] f[i](fs,l,delta) + | sum2[i](delta) = SUM[fs,l] (f[i](fs,l,delta).nf(feature_vector(fs,l))) + | f[i](fs,l,delta) = (classifier.prob_classify(fs).prob(l) . + | feature_vector(fs,l)[i] . + | exp(delta[i] . nf(feature_vector(fs,l)))) + + Note that *sum1* and *sum2* depend on ``delta``; so they need + to be re-computed each iteration. + + The variables ``nfmap``, ``nfarray``, and ``nftranspose`` are + used to generate a dense encoding for *nf(ltext)*. This + allows ``_deltas`` to calculate *sum1* and *sum2* using + matrices, which yields a significant performance improvement. + + :param train_toks: The set of training tokens. + :type train_toks: list(tuple(dict, str)) + :param classifier: The current classifier. + :type classifier: ClassifierI + :param ffreq_empirical: An array containing the empirical + frequency for each feature. The *i*\ th element of this + array is the empirical frequency for feature *i*. + :type ffreq_empirical: sequence of float + :param unattested: An array that is 1 for features that are + not attested in the training data; and 0 for features that + are attested. In other words, ``unattested[i]==0`` iff + ``ffreq_empirical[i]==0``. + :type unattested: sequence of int + :param nfmap: A map that can be used to compress ``nf`` to a dense + vector. + :type nfmap: dict(int -> int) + :param nfarray: An array that can be used to uncompress ``nf`` + from a dense vector. + :type nfarray: array(float) + :param nftranspose: The transpose of ``nfarray`` + :type nftranspose: array(float) + """ + # These parameters control when we decide that we've + # converged. It probably should be possible to set these + # manually, via keyword arguments to train. + NEWTON_CONVERGE = 1e-12 + MAX_NEWTON = 300 + + deltas = numpy.ones(encoding.length(), "d") + + # Precompute the A matrix: + # A[nf][id] = sum ( p(fs) * p(label|fs) * f(fs,label) ) + # over all label,fs s.t. num_features[label,fs]=nf + A = numpy.zeros((len(nfmap), encoding.length()), "d") + + for tok, label in train_toks: + dist = classifier.prob_classify(tok) + + for label in encoding.labels(): + # Generate the feature vector + feature_vector = encoding.encode(tok, label) + # Find the number of active features + nf = sum(val for (id, val) in feature_vector) + # Update the A matrix + for id, val in feature_vector: + A[nfmap[nf], id] += dist.prob(label) * val + A /= len(train_toks) + + # Iteratively solve for delta. Use the following variables: + # - nf_delta[x][y] = nfarray[x] * delta[y] + # - exp_nf_delta[x][y] = exp(nf[x] * delta[y]) + # - nf_exp_nf_delta[x][y] = nf[x] * exp(nf[x] * delta[y]) + # - sum1[i][nf] = sum p(fs)p(label|fs)f[i](label,fs) + # exp(delta[i]nf) + # - sum2[i][nf] = sum p(fs)p(label|fs)f[i](label,fs) + # nf exp(delta[i]nf) + for rangenum in range(MAX_NEWTON): + nf_delta = numpy.outer(nfarray, deltas) + exp_nf_delta = 2**nf_delta + nf_exp_nf_delta = nftranspose * exp_nf_delta + sum1 = numpy.sum(exp_nf_delta * A, axis=0) + sum2 = numpy.sum(nf_exp_nf_delta * A, axis=0) + + # Avoid division by zero. + for fid in unattested: + sum2[fid] += 1 + + # Update the deltas. + deltas -= (ffreq_empirical - sum1) / -sum2 + + # We can stop once we converge. + n_error = numpy.sum(abs(ffreq_empirical - sum1)) / numpy.sum(abs(deltas)) + if n_error < NEWTON_CONVERGE: + return deltas + + return deltas + + +###################################################################### +# { Classifier Trainer: megam +###################################################################### + + +# [xx] possible extension: add support for using implicit file format; +# this would need to put requirements on what encoding is used. But +# we may need this for other maxent classifier trainers that require +# implicit formats anyway. +def train_maxent_classifier_with_megam( + train_toks, trace=3, encoding=None, labels=None, gaussian_prior_sigma=0, **kwargs +): + """ + Train a new ``ConditionalExponentialClassifier``, using the given + training samples, using the external ``megam`` library. This + ``ConditionalExponentialClassifier`` will encode the model that + maximizes entropy from all the models that are empirically + consistent with ``train_toks``. + + :see: ``train_maxent_classifier()`` for parameter descriptions. + :see: ``nltk.classify.megam`` + """ + + explicit = True + bernoulli = True + if "explicit" in kwargs: + explicit = kwargs["explicit"] + if "bernoulli" in kwargs: + bernoulli = kwargs["bernoulli"] + + # Construct an encoding from the training data. + if encoding is None: + # Count cutoff can also be controlled by megam with the -minfc + # option. Not sure where the best place for it is. + count_cutoff = kwargs.get("count_cutoff", 0) + encoding = BinaryMaxentFeatureEncoding.train( + train_toks, count_cutoff, labels=labels, alwayson_features=True + ) + elif labels is not None: + raise ValueError("Specify encoding or labels, not both") + + # Write a training file for megam. + try: + fd, trainfile_name = tempfile.mkstemp(prefix="nltk-") + with open(trainfile_name, "w") as trainfile: + write_megam_file( + train_toks, encoding, trainfile, explicit=explicit, bernoulli=bernoulli + ) + os.close(fd) + except (OSError, ValueError) as e: + raise ValueError("Error while creating megam training file: %s" % e) from e + + # Run megam on the training file. + options = [] + options += ["-nobias", "-repeat", "10"] + if explicit: + options += ["-explicit"] + if not bernoulli: + options += ["-fvals"] + if gaussian_prior_sigma: + # Lambda is just the precision of the Gaussian prior, i.e. it's the + # inverse variance, so the parameter conversion is 1.0/sigma**2. + # See https://users.umiacs.umd.edu/~hal/docs/daume04cg-bfgs.pdf + inv_variance = 1.0 / gaussian_prior_sigma**2 + else: + inv_variance = 0 + options += ["-lambda", "%.2f" % inv_variance, "-tune"] + if trace < 3: + options += ["-quiet"] + if "max_iter" in kwargs: + options += ["-maxi", "%s" % kwargs["max_iter"]] + if "ll_delta" in kwargs: + # [xx] this is actually a perplexity delta, not a log + # likelihood delta + options += ["-dpp", "%s" % abs(kwargs["ll_delta"])] + if hasattr(encoding, "cost"): + options += ["-multilabel"] # each possible la + options += ["multiclass", trainfile_name] + stdout = call_megam(options) + # print('./megam_i686.opt ', ' '.join(options)) + # Delete the training file + try: + os.remove(trainfile_name) + except OSError as e: + print(f"Warning: unable to delete {trainfile_name}: {e}") + + # Parse the generated weight vector. + weights = parse_megam_weights(stdout, encoding.length(), explicit) + + # Convert from base-e to base-2 weights. + weights *= numpy.log2(numpy.e) + + # Build the classifier + return MaxentClassifier(encoding, weights) + + +###################################################################### +# { Classifier Trainer: tadm +###################################################################### + + +class TadmMaxentClassifier(MaxentClassifier): + @classmethod + def train(cls, train_toks, **kwargs): + algorithm = kwargs.get("algorithm", "tao_lmvm") + trace = kwargs.get("trace", 3) + encoding = kwargs.get("encoding", None) + labels = kwargs.get("labels", None) + sigma = kwargs.get("gaussian_prior_sigma", 0) + count_cutoff = kwargs.get("count_cutoff", 0) + max_iter = kwargs.get("max_iter") + ll_delta = kwargs.get("min_lldelta") + + # Construct an encoding from the training data. + if not encoding: + encoding = TadmEventMaxentFeatureEncoding.train( + train_toks, count_cutoff, labels=labels + ) + + trainfile_fd, trainfile_name = tempfile.mkstemp( + prefix="nltk-tadm-events-", suffix=".gz" + ) + weightfile_fd, weightfile_name = tempfile.mkstemp(prefix="nltk-tadm-weights-") + + trainfile = gzip_open_unicode(trainfile_name, "w") + write_tadm_file(train_toks, encoding, trainfile) + trainfile.close() + + options = [] + options.extend(["-monitor"]) + options.extend(["-method", algorithm]) + if sigma: + options.extend(["-l2", "%.6f" % sigma**2]) + if max_iter: + options.extend(["-max_it", "%d" % max_iter]) + if ll_delta: + options.extend(["-fatol", "%.6f" % abs(ll_delta)]) + options.extend(["-events_in", trainfile_name]) + options.extend(["-params_out", weightfile_name]) + if trace < 3: + options.extend(["2>&1"]) + else: + options.extend(["-summary"]) + + call_tadm(options) + + with open(weightfile_name) as weightfile: + weights = parse_tadm_weights(weightfile) + + os.remove(trainfile_name) + os.remove(weightfile_name) + + # Convert from base-e to base-2 weights. + weights *= numpy.log2(numpy.e) + + # Build the classifier + return cls(encoding, weights) + + +###################################################################### +# Load/Save Classifier Parameters as Tab-files +###################################################################### + + +def load_maxent_params(tab_dir): + import numpy + + from nltk.data import open_datafile + from nltk.tabdata import MaxentDecoder + + mdec = MaxentDecoder() + # Use .join() to reach the files regardless of zip/real FS. + with open_datafile(tab_dir, "weights.txt") as f: + wgt = numpy.array(list(map(numpy.float64, mdec.txt2list(f)))) + + with open_datafile(tab_dir, "mapping.tab") as f: + mpg = mdec.tupkey2dict(f) + + with open_datafile(tab_dir, "labels.txt") as f: + lab = mdec.txt2list(f) + + with open_datafile(tab_dir, "alwayson.tab") as f: + aon = mdec.tab2ivdict(f) + + return wgt, mpg, lab, aon + + +def save_maxent_params(wgt, mpg, lab, aon, tab_dir="/tmp"): + + from os import mkdir + from os.path import isdir + + from nltk.tabdata import MaxentEncoder + + menc = MaxentEncoder() + if not isdir(tab_dir): + mkdir(tab_dir) + + print(f"Saving Maxent parameters in {tab_dir}") + + with open(f"{tab_dir}/weights.txt", "w") as f: + f.write(f"{menc.list2txt(map(repr, wgt.tolist()))}") + with open(f"{tab_dir}/mapping.tab", "w") as f: + f.write(f"{menc.tupdict2tab(mpg)}") + with open(f"{tab_dir}/labels.txt", "w") as f: + f.write(f"{menc.list2txt(lab)}") + with open(f"{tab_dir}/alwayson.tab", "w") as f: + f.write(f"{menc.ivdict2tab(aon)}") + + +def maxent_pos_tagger(): + from nltk.data import find + from nltk.tag.sequential import ClassifierBasedPOSTagger + + tab_dir = find("taggers/maxent_treebank_pos_tagger_tab/english/") + wgt, mpg, lab, aon = load_maxent_params(tab_dir) + mc = MaxentClassifier( + BinaryMaxentFeatureEncoding(lab, mpg, alwayson_features=aon), wgt + ) + return ClassifierBasedPOSTagger(classifier=mc) + + +###################################################################### +# { Demo +###################################################################### +def demo(): + from nltk.classify.util import names_demo + + classifier = names_demo(MaxentClassifier.train) + + +if __name__ == "__main__": + demo() diff --git a/nltk/classify/megam.py b/nltk/classify/megam.py new file mode 100644 index 0000000..e7efb1f --- /dev/null +++ b/nltk/classify/megam.py @@ -0,0 +1,184 @@ +# Natural Language Toolkit: Interface to Megam Classifier +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A set of functions used to interface with the external megam_ maxent +optimization package. Before megam can be used, you should tell NLTK where it +can find the megam binary, using the ``config_megam()`` function. Typical +usage: + + >>> from nltk.classify import megam + >>> megam.config_megam() # pass path to megam if not found in PATH # doctest: +SKIP + [Found megam: ...] + +Use with MaxentClassifier. Example below, see MaxentClassifier documentation +for details. + + nltk.classify.MaxentClassifier.train(corpus, 'megam') + +.. _megam: https://www.umiacs.umd.edu/~hal/megam/index.html +""" +import subprocess + +from nltk.internals import find_binary + +try: + import numpy +except ImportError: + numpy = None + +###################################################################### +# { Configuration +###################################################################### + +_megam_bin = None + + +def config_megam(bin=None): + """ + Configure NLTK's interface to the ``megam`` maxent optimization + package. + + :param bin: The full path to the ``megam`` binary. If not specified, + then nltk will search the system for a ``megam`` binary; and if + one is not found, it will raise a ``LookupError`` exception. + :type bin: str + """ + global _megam_bin + _megam_bin = find_binary( + "megam", + bin, + env_vars=["MEGAM"], + binary_names=["megam.opt", "megam", "megam_686", "megam_i686.opt"], + url="https://www.umiacs.umd.edu/~hal/megam/index.html", + ) + + +###################################################################### +# { Megam Interface Functions +###################################################################### + + +def write_megam_file(train_toks, encoding, stream, bernoulli=True, explicit=True): + """ + Generate an input file for ``megam`` based on the given corpus of + classified tokens. + + :type train_toks: list(tuple(dict, str)) + :param train_toks: Training data, represented as a list of + pairs, the first member of which is a feature dictionary, + and the second of which is a classification label. + + :type encoding: MaxentFeatureEncodingI + :param encoding: A feature encoding, used to convert featuresets + into feature vectors. May optionally implement a cost() method + in order to assign different costs to different class predictions. + + :type stream: stream + :param stream: The stream to which the megam input file should be + written. + + :param bernoulli: If true, then use the 'bernoulli' format. I.e., + all joint features have binary values, and are listed iff they + are true. Otherwise, list feature values explicitly. If + ``bernoulli=False``, then you must call ``megam`` with the + ``-fvals`` option. + + :param explicit: If true, then use the 'explicit' format. I.e., + list the features that would fire for any of the possible + labels, for each token. If ``explicit=True``, then you must + call ``megam`` with the ``-explicit`` option. + """ + # Look up the set of labels. + labels = encoding.labels() + labelnum = {label: i for (i, label) in enumerate(labels)} + + # Write the file, which contains one line per instance. + for featureset, label in train_toks: + # First, the instance number (or, in the weighted multiclass case, the cost of each label). + if hasattr(encoding, "cost"): + stream.write( + ":".join(str(encoding.cost(featureset, label, l)) for l in labels) + ) + else: + stream.write("%d" % labelnum[label]) + + # For implicit file formats, just list the features that fire + # for this instance's actual label. + if not explicit: + _write_megam_features(encoding.encode(featureset, label), stream, bernoulli) + + # For explicit formats, list the features that would fire for + # any of the possible labels. + else: + for l in labels: + stream.write(" #") + _write_megam_features(encoding.encode(featureset, l), stream, bernoulli) + + # End of the instance. + stream.write("\n") + + +def parse_megam_weights(s, features_count, explicit=True): + """ + Given the stdout output generated by ``megam`` when training a + model, return a ``numpy`` array containing the corresponding weight + vector. This function does not currently handle bias features. + """ + if numpy is None: + raise ValueError("This function requires that numpy be installed") + assert explicit, "non-explicit not supported yet" + lines = s.strip().split("\n") + weights = numpy.zeros(features_count, "d") + for line in lines: + if line.strip(): + fid, weight = line.split() + weights[int(fid)] = float(weight) + return weights + + +def _write_megam_features(vector, stream, bernoulli): + if not vector: + raise ValueError( + "MEGAM classifier requires the use of an " "always-on feature." + ) + for fid, fval in vector: + if bernoulli: + if fval == 1: + stream.write(" %s" % fid) + elif fval != 0: + raise ValueError( + "If bernoulli=True, then all" "features must be binary." + ) + else: + stream.write(f" {fid} {fval}") + + +def call_megam(args): + """ + Call the ``megam`` binary with the given arguments. + """ + if isinstance(args, str): + raise TypeError("args should be a list of strings") + if _megam_bin is None: + config_megam() + + # Call megam via a subprocess + cmd = [_megam_bin] + args + p = subprocess.Popen(cmd, stdout=subprocess.PIPE) + (stdout, stderr) = p.communicate() + + # Check the return code. + if p.returncode != 0: + print() + print(stderr) + raise OSError("megam command failed!") + + if isinstance(stdout, str): + return stdout + else: + return stdout.decode("utf-8") diff --git a/nltk/classify/naivebayes.py b/nltk/classify/naivebayes.py new file mode 100644 index 0000000..3e2e051 --- /dev/null +++ b/nltk/classify/naivebayes.py @@ -0,0 +1,260 @@ +# Natural Language Toolkit: Naive Bayes Classifiers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A classifier based on the Naive Bayes algorithm. In order to find the +probability for a label, this algorithm first uses the Bayes rule to +express P(label|features) in terms of P(label) and P(features|label): + +| P(label) * P(features|label) +| P(label|features) = ------------------------------ +| P(features) + +The algorithm then makes the 'naive' assumption that all features are +independent, given the label: + +| P(label) * P(f1|label) * ... * P(fn|label) +| P(label|features) = -------------------------------------------- +| P(features) + +Rather than computing P(features) explicitly, the algorithm just +calculates the numerator for each label, and normalizes them so they +sum to one: + +| P(label) * P(f1|label) * ... * P(fn|label) +| P(label|features) = -------------------------------------------- +| SUM[l]( P(l) * P(f1|l) * ... * P(fn|l) ) +""" + +from collections import defaultdict + +from nltk.classify.api import ClassifierI +from nltk.probability import DictionaryProbDist, ELEProbDist, FreqDist, sum_logs + +##////////////////////////////////////////////////////// +## Naive Bayes Classifier +##////////////////////////////////////////////////////// + + +class NaiveBayesClassifier(ClassifierI): + """ + A Naive Bayes classifier. Naive Bayes classifiers are + paramaterized by two probability distributions: + + - P(label) gives the probability that an input will receive each + label, given no information about the input's features. + + - P(fname=fval|label) gives the probability that a given feature + (fname) will receive a given value (fval), given that the + label (label). + + If the classifier encounters an input with a feature that has + never been seen with any label, then rather than assigning a + probability of 0 to all labels, it will ignore that feature. + + The feature value 'None' is reserved for unseen feature values; + you generally should not use 'None' as a feature value for one of + your own features. + """ + + def __init__(self, label_probdist, feature_probdist): + """ + :param label_probdist: P(label), the probability distribution + over labels. It is expressed as a ``ProbDistI`` whose + samples are labels. I.e., P(label) = + ``label_probdist.prob(label)``. + + :param feature_probdist: P(fname=fval|label), the probability + distribution for feature values, given labels. It is + expressed as a dictionary whose keys are ``(label, fname)`` + pairs and whose values are ``ProbDistI`` objects over feature + values. I.e., P(fname=fval|label) = + ``feature_probdist[label,fname].prob(fval)``. If a given + ``(label,fname)`` is not a key in ``feature_probdist``, then + it is assumed that the corresponding P(fname=fval|label) + is 0 for all values of ``fval``. + """ + self._label_probdist = label_probdist + self._feature_probdist = feature_probdist + self._labels = list(label_probdist.samples()) + + def labels(self): + return self._labels + + def classify(self, featureset): + return self.prob_classify(featureset).max() + + def prob_classify(self, featureset): + # Discard any feature names that we've never seen before. + # Otherwise, we'll just assign a probability of 0 to + # everything. + featureset = featureset.copy() + for fname in list(featureset.keys()): + for label in self._labels: + if (label, fname) in self._feature_probdist: + break + else: + # print('Ignoring unseen feature %s' % fname) + del featureset[fname] + + # Find the log probability of each label, given the features. + # Start with the log probability of the label itself. + logprob = {} + for label in self._labels: + logprob[label] = self._label_probdist.logprob(label) + + # Then add in the log probability of features given labels. + for label in self._labels: + for fname, fval in featureset.items(): + if (label, fname) in self._feature_probdist: + feature_probs = self._feature_probdist[label, fname] + logprob[label] += feature_probs.logprob(fval) + else: + # nb: This case will never come up if the + # classifier was created by + # NaiveBayesClassifier.train(). + logprob[label] += sum_logs([]) # = -INF. + + return DictionaryProbDist(logprob, normalize=True, log=True) + + def show_most_informative_features(self, n=10): + # Determine the most relevant features, and display them. + cpdist = self._feature_probdist + print("Most Informative Features") + + for fname, fval in self.most_informative_features(n): + + def labelprob(l): + return cpdist[l, fname].prob(fval) + + labels = sorted( + (l for l in self._labels if fval in cpdist[l, fname].samples()), + key=lambda element: (-labelprob(element), element), + reverse=True, + ) + if len(labels) == 1: + continue + l0 = labels[0] + l1 = labels[-1] + if cpdist[l0, fname].prob(fval) == 0: + ratio = "INF" + else: + ratio = "%8.1f" % ( + cpdist[l1, fname].prob(fval) / cpdist[l0, fname].prob(fval) + ) + print( + "%24s = %-14r %6s : %-6s = %s : 1.0" + % (fname, fval, ("%s" % l1)[:6], ("%s" % l0)[:6], ratio) + ) + + def most_informative_features(self, n=100): + """ + Return a list of the 'most informative' features used by this + classifier. For the purpose of this function, the + informativeness of a feature ``(fname,fval)`` is equal to the + highest value of P(fname=fval|label), for any label, divided by + the lowest value of P(fname=fval|label), for any label: + + | max[ P(fname=fval|label1) / P(fname=fval|label2) ] + """ + if hasattr(self, "_most_informative_features"): + return self._most_informative_features[:n] + else: + # The set of (fname, fval) pairs used by this classifier. + features = set() + # The max & min probability associated w/ each (fname, fval) + # pair. Maps (fname,fval) -> float. + maxprob = defaultdict(float) + minprob = defaultdict(lambda: 1.0) + + for (label, fname), probdist in self._feature_probdist.items(): + for fval in probdist.samples(): + feature = (fname, fval) + features.add(feature) + p = probdist.prob(fval) + maxprob[feature] = max(p, maxprob[feature]) + minprob[feature] = min(p, minprob[feature]) + if minprob[feature] == 0: + features.discard(feature) + + # Convert features to a list, & sort it by how informative + # features are. + self._most_informative_features = sorted( + features, + key=lambda feature_: ( + minprob[feature_] / maxprob[feature_], + feature_[0], + feature_[1] in [None, False, True], + str(feature_[1]).lower(), + ), + ) + return self._most_informative_features[:n] + + @classmethod + def train(cls, labeled_featuresets, estimator=ELEProbDist): + """ + :param labeled_featuresets: A list of classified featuresets, + i.e., a list of tuples ``(featureset, label)``. + """ + label_freqdist = FreqDist() + feature_freqdist = defaultdict(FreqDist) + feature_values = defaultdict(set) + fnames = set() + + # Count up how many times each feature value occurred, given + # the label and featurename. + for featureset, label in labeled_featuresets: + label_freqdist[label] += 1 + for fname, fval in featureset.items(): + # Increment freq(fval|label, fname) + feature_freqdist[label, fname][fval] += 1 + # Record that fname can take the value fval. + feature_values[fname].add(fval) + # Keep a list of all feature names. + fnames.add(fname) + + # If a feature didn't have a value given for an instance, then + # we assume that it gets the implicit value 'None.' This loop + # counts up the number of 'missing' feature values for each + # (label,fname) pair, and increments the count of the fval + # 'None' by that amount. + for label in label_freqdist: + num_samples = label_freqdist[label] + for fname in fnames: + count = feature_freqdist[label, fname].N() + # Only add a None key when necessary, i.e. if there are + # any samples with feature 'fname' missing. + if num_samples - count > 0: + feature_freqdist[label, fname][None] += num_samples - count + feature_values[fname].add(None) + + # Create the P(label) distribution + label_probdist = estimator(label_freqdist) + + # Create the P(fval|label, fname) distribution + feature_probdist = {} + for (label, fname), freqdist in feature_freqdist.items(): + probdist = estimator(freqdist, bins=len(feature_values[fname])) + feature_probdist[label, fname] = probdist + + return cls(label_probdist, feature_probdist) + + +##////////////////////////////////////////////////////// +## Demo +##////////////////////////////////////////////////////// + + +def demo(): + from nltk.classify.util import names_demo + + classifier = names_demo(NaiveBayesClassifier.train) + classifier.show_most_informative_features() + + +if __name__ == "__main__": + demo() diff --git a/nltk/classify/positivenaivebayes.py b/nltk/classify/positivenaivebayes.py new file mode 100644 index 0000000..cc303cc --- /dev/null +++ b/nltk/classify/positivenaivebayes.py @@ -0,0 +1,180 @@ +# Natural Language Toolkit: Positive Naive Bayes Classifier +# +# Copyright (C) 2012 NLTK Project +# Author: Alessandro Presta +# URL: +# For license information, see LICENSE.TXT + +""" +A variant of the Naive Bayes Classifier that performs binary classification with +partially-labeled training sets. In other words, assume we want to build a classifier +that assigns each example to one of two complementary classes (e.g., male names and +female names). +If we have a training set with labeled examples for both classes, we can use a +standard Naive Bayes Classifier. However, consider the case when we only have labeled +examples for one of the classes, and other, unlabeled, examples. +Then, assuming a prior distribution on the two labels, we can use the unlabeled set +to estimate the frequencies of the various features. + +Let the two possible labels be 1 and 0, and let's say we only have examples labeled 1 +and unlabeled examples. We are also given an estimate of P(1). + +We compute P(feature|1) exactly as in the standard case. + +To compute P(feature|0), we first estimate P(feature) from the unlabeled set (we are +assuming that the unlabeled examples are drawn according to the given prior distribution) +and then express the conditional probability as: + +| P(feature) - P(feature|1) * P(1) +| P(feature|0) = ---------------------------------- +| P(0) + +Example: + + >>> from nltk.classify import PositiveNaiveBayesClassifier + +Some sentences about sports: + + >>> sports_sentences = [ 'The team dominated the game', + ... 'They lost the ball', + ... 'The game was intense', + ... 'The goalkeeper catched the ball', + ... 'The other team controlled the ball' ] + +Mixed topics, including sports: + + >>> various_sentences = [ 'The President did not comment', + ... 'I lost the keys', + ... 'The team won the game', + ... 'Sara has two kids', + ... 'The ball went off the court', + ... 'They had the ball for the whole game', + ... 'The show is over' ] + +The features of a sentence are simply the words it contains: + + >>> def features(sentence): + ... words = sentence.lower().split() + ... return dict(('contains(%s)' % w, True) for w in words) + +We use the sports sentences as positive examples, the mixed ones ad unlabeled examples: + + >>> positive_featuresets = map(features, sports_sentences) + >>> unlabeled_featuresets = map(features, various_sentences) + >>> classifier = PositiveNaiveBayesClassifier.train(positive_featuresets, + ... unlabeled_featuresets) + +Is the following sentence about sports? + + >>> classifier.classify(features('The cat is on the table')) + False + +What about this one? + + >>> classifier.classify(features('My team lost the game')) + True +""" + +from collections import defaultdict + +from nltk.classify.naivebayes import NaiveBayesClassifier +from nltk.probability import DictionaryProbDist, ELEProbDist, FreqDist + +##////////////////////////////////////////////////////// +## Positive Naive Bayes Classifier +##////////////////////////////////////////////////////// + + +class PositiveNaiveBayesClassifier(NaiveBayesClassifier): + @staticmethod + def train( + positive_featuresets, + unlabeled_featuresets, + positive_prob_prior=0.5, + estimator=ELEProbDist, + ): + """ + :param positive_featuresets: An iterable of featuresets that are known as positive + examples (i.e., their label is ``True``). + + :param unlabeled_featuresets: An iterable of featuresets whose label is unknown. + + :param positive_prob_prior: A prior estimate of the probability of the label + ``True`` (default 0.5). + """ + positive_feature_freqdist = defaultdict(FreqDist) + unlabeled_feature_freqdist = defaultdict(FreqDist) + feature_values = defaultdict(set) + fnames = set() + + # Count up how many times each feature value occurred in positive examples. + num_positive_examples = 0 + for featureset in positive_featuresets: + for fname, fval in featureset.items(): + positive_feature_freqdist[fname][fval] += 1 + feature_values[fname].add(fval) + fnames.add(fname) + num_positive_examples += 1 + + # Count up how many times each feature value occurred in unlabeled examples. + num_unlabeled_examples = 0 + for featureset in unlabeled_featuresets: + for fname, fval in featureset.items(): + unlabeled_feature_freqdist[fname][fval] += 1 + feature_values[fname].add(fval) + fnames.add(fname) + num_unlabeled_examples += 1 + + # If a feature didn't have a value given for an instance, then we assume that + # it gets the implicit value 'None'. + for fname in fnames: + count = positive_feature_freqdist[fname].N() + positive_feature_freqdist[fname][None] += num_positive_examples - count + feature_values[fname].add(None) + + for fname in fnames: + count = unlabeled_feature_freqdist[fname].N() + unlabeled_feature_freqdist[fname][None] += num_unlabeled_examples - count + feature_values[fname].add(None) + + negative_prob_prior = 1.0 - positive_prob_prior + + # Create the P(label) distribution. + label_probdist = DictionaryProbDist( + {True: positive_prob_prior, False: negative_prob_prior} + ) + + # Create the P(fval|label, fname) distribution. + feature_probdist = {} + for fname, freqdist in positive_feature_freqdist.items(): + probdist = estimator(freqdist, bins=len(feature_values[fname])) + feature_probdist[True, fname] = probdist + + for fname, freqdist in unlabeled_feature_freqdist.items(): + global_probdist = estimator(freqdist, bins=len(feature_values[fname])) + negative_feature_probs = {} + for fval in feature_values[fname]: + prob = ( + global_probdist.prob(fval) + - positive_prob_prior * feature_probdist[True, fname].prob(fval) + ) / negative_prob_prior + # TODO: We need to add some kind of smoothing here, instead of + # setting negative probabilities to zero and normalizing. + negative_feature_probs[fval] = max(prob, 0.0) + feature_probdist[False, fname] = DictionaryProbDist( + negative_feature_probs, normalize=True + ) + + return PositiveNaiveBayesClassifier(label_probdist, feature_probdist) + + +##////////////////////////////////////////////////////// +## Demo +##////////////////////////////////////////////////////// + + +def demo(): + from nltk.classify.util import partial_names_demo + + classifier = partial_names_demo(PositiveNaiveBayesClassifier.train) + classifier.show_most_informative_features() diff --git a/nltk/classify/rte_classify.py b/nltk/classify/rte_classify.py new file mode 100644 index 0000000..2c146c3 --- /dev/null +++ b/nltk/classify/rte_classify.py @@ -0,0 +1,183 @@ +# Natural Language Toolkit: RTE Classifier +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +""" +Simple classifier for RTE corpus. + +It calculates the overlap in words and named entities between text and +hypothesis, and also whether there are words / named entities in the +hypothesis which fail to occur in the text, since this is an indicator that +the hypothesis is more informative than (i.e not entailed by) the text. + +TO DO: better Named Entity classification +TO DO: add lemmatization +""" + +from nltk.classify.maxent import MaxentClassifier +from nltk.classify.util import accuracy +from nltk.tokenize import RegexpTokenizer + + +class RTEFeatureExtractor: + """ + This builds a bag of words for both the text and the hypothesis after + throwing away some stopwords, then calculates overlap and difference. + """ + + def __init__(self, rtepair, stop=True, use_lemmatize=False): + """ + :param rtepair: a ``RTEPair`` from which features should be extracted + :param stop: if ``True``, stopwords are thrown away. + :type stop: bool + """ + self.stop = stop + self.stopwords = { + "a", + "the", + "it", + "they", + "of", + "in", + "to", + "is", + "have", + "are", + "were", + "and", + "very", + ".", + ",", + } + + self.negwords = {"no", "not", "never", "failed", "rejected", "denied"} + # Try to tokenize so that abbreviations, monetary amounts, email + # addresses, URLs are single tokens. + tokenizer = RegexpTokenizer(r"[\w.@:/]+|\w+|\$[\d.]+") + + # Get the set of word types for text and hypothesis + self.text_tokens = tokenizer.tokenize(rtepair.text) + self.hyp_tokens = tokenizer.tokenize(rtepair.hyp) + self.text_words = set(self.text_tokens) + self.hyp_words = set(self.hyp_tokens) + + if use_lemmatize: + self.text_words = {self._lemmatize(token) for token in self.text_tokens} + self.hyp_words = {self._lemmatize(token) for token in self.hyp_tokens} + + if self.stop: + self.text_words = self.text_words - self.stopwords + self.hyp_words = self.hyp_words - self.stopwords + + self._overlap = self.hyp_words & self.text_words + self._hyp_extra = self.hyp_words - self.text_words + self._txt_extra = self.text_words - self.hyp_words + + def overlap(self, toktype, debug=False): + """ + Compute the overlap between text and hypothesis. + + :param toktype: distinguish Named Entities from ordinary words + :type toktype: 'ne' or 'word' + """ + ne_overlap = {token for token in self._overlap if self._ne(token)} + if toktype == "ne": + if debug: + print("ne overlap", ne_overlap) + return ne_overlap + elif toktype == "word": + if debug: + print("word overlap", self._overlap - ne_overlap) + return self._overlap - ne_overlap + else: + raise ValueError("Type not recognized:'%s'" % toktype) + + def hyp_extra(self, toktype, debug=True): + """ + Compute the extraneous material in the hypothesis. + + :param toktype: distinguish Named Entities from ordinary words + :type toktype: 'ne' or 'word' + """ + ne_extra = {token for token in self._hyp_extra if self._ne(token)} + if toktype == "ne": + return ne_extra + elif toktype == "word": + return self._hyp_extra - ne_extra + else: + raise ValueError("Type not recognized: '%s'" % toktype) + + @staticmethod + def _ne(token): + """ + This just assumes that words in all caps or titles are + named entities. + + :type token: str + """ + if token.istitle() or token.isupper(): + return True + return False + + @staticmethod + def _lemmatize(word): + """ + Use morphy from WordNet to find the base form of verbs. + """ + from nltk.corpus import wordnet as wn + + lemma = wn.morphy(word, pos=wn.VERB) + if lemma is not None: + return lemma + return word + + +def rte_features(rtepair): + extractor = RTEFeatureExtractor(rtepair) + features = {} + features["alwayson"] = True + features["word_overlap"] = len(extractor.overlap("word")) + features["word_hyp_extra"] = len(extractor.hyp_extra("word")) + features["ne_overlap"] = len(extractor.overlap("ne")) + features["ne_hyp_extra"] = len(extractor.hyp_extra("ne")) + features["neg_txt"] = len(extractor.negwords & extractor.text_words) + features["neg_hyp"] = len(extractor.negwords & extractor.hyp_words) + return features + + +def rte_featurize(rte_pairs): + return [(rte_features(pair), pair.value) for pair in rte_pairs] + + +def rte_classifier(algorithm, sample_N=None): + from nltk.corpus import rte as rte_corpus + + train_set = rte_corpus.pairs(["rte1_dev.xml", "rte2_dev.xml", "rte3_dev.xml"]) + test_set = rte_corpus.pairs(["rte1_test.xml", "rte2_test.xml", "rte3_test.xml"]) + + if sample_N is not None: + train_set = train_set[:sample_N] + test_set = test_set[:sample_N] + + featurized_train_set = rte_featurize(train_set) + featurized_test_set = rte_featurize(test_set) + + # Train the classifier + print("Training classifier...") + if algorithm in ["megam"]: # MEGAM based algorithms. + clf = MaxentClassifier.train(featurized_train_set, algorithm) + elif algorithm in ["GIS", "IIS"]: # Use default GIS/IIS MaxEnt algorithm + clf = MaxentClassifier.train(featurized_train_set, algorithm) + else: + err_msg = str( + "RTEClassifier only supports these algorithms:\n " + "'megam', 'GIS', 'IIS'.\n" + ) + raise Exception(err_msg) + print("Testing classifier...") + acc = accuracy(clf, featurized_test_set) + print("Accuracy: %6.4f" % acc) + return clf diff --git a/nltk/classify/scikitlearn.py b/nltk/classify/scikitlearn.py new file mode 100644 index 0000000..95afc70 --- /dev/null +++ b/nltk/classify/scikitlearn.py @@ -0,0 +1,143 @@ +# Natural Language Toolkit: Interface to scikit-learn classifiers +# +# Author: Lars Buitinck +# URL: +# For license information, see LICENSE.TXT +""" +scikit-learn (https://scikit-learn.org) is a machine learning library for +Python. It supports many classification algorithms, including SVMs, +Naive Bayes, logistic regression (MaxEnt) and decision trees. + +This package implements a wrapper around scikit-learn classifiers. To use this +wrapper, construct a scikit-learn estimator object, then use that to construct +a SklearnClassifier. E.g., to wrap a linear SVM with default settings: + +>>> from sklearn.svm import LinearSVC +>>> from nltk.classify.scikitlearn import SklearnClassifier +>>> classif = SklearnClassifier(LinearSVC()) + +A scikit-learn classifier may include preprocessing steps when it's wrapped +in a Pipeline object. The following constructs and wraps a Naive Bayes text +classifier with tf-idf weighting and chi-square feature selection to get the +best 1000 features: + +>>> from sklearn.feature_extraction.text import TfidfTransformer +>>> from sklearn.feature_selection import SelectKBest, chi2 +>>> from sklearn.naive_bayes import MultinomialNB +>>> from sklearn.pipeline import Pipeline +>>> pipeline = Pipeline([('tfidf', TfidfTransformer()), +... ('chi2', SelectKBest(chi2, k=1000)), +... ('nb', MultinomialNB())]) +>>> classif = SklearnClassifier(pipeline) +""" + +from nltk.classify.api import ClassifierI +from nltk.probability import DictionaryProbDist + +try: + from sklearn.feature_extraction import DictVectorizer + from sklearn.preprocessing import LabelEncoder +except ImportError: + pass + +__all__ = ["SklearnClassifier"] + + +class SklearnClassifier(ClassifierI): + """Wrapper for scikit-learn classifiers.""" + + def __init__(self, estimator, dtype=float, sparse=True): + """ + :param estimator: scikit-learn classifier object. + + :param dtype: data type used when building feature array. + scikit-learn estimators work exclusively on numeric data. The + default value should be fine for almost all situations. + + :param sparse: Whether to use sparse matrices internally. + The estimator must support these; not all scikit-learn classifiers + do (see their respective documentation and look for "sparse + matrix"). The default value is True, since most NLP problems + involve sparse feature sets. Setting this to False may take a + great amount of memory. + :type sparse: boolean. + """ + self._clf = estimator + self._encoder = LabelEncoder() + self._vectorizer = DictVectorizer(dtype=dtype, sparse=sparse) + + def __repr__(self): + return "" % self._clf + + def classify_many(self, featuresets): + """Classify a batch of samples. + + :param featuresets: An iterable over featuresets, each a dict mapping + strings to either numbers, booleans or strings. + :return: The predicted class label for each input sample. + :rtype: list + """ + X = self._vectorizer.transform(featuresets) + classes = self._encoder.classes_ + return [classes[i] for i in self._clf.predict(X)] + + def prob_classify_many(self, featuresets): + """Compute per-class probabilities for a batch of samples. + + :param featuresets: An iterable over featuresets, each a dict mapping + strings to either numbers, booleans or strings. + :rtype: list of ``ProbDistI`` + """ + X = self._vectorizer.transform(featuresets) + y_proba_list = self._clf.predict_proba(X) + return [self._make_probdist(y_proba) for y_proba in y_proba_list] + + def labels(self): + """The class labels used by this classifier. + + :rtype: list + """ + return list(self._encoder.classes_) + + def train(self, labeled_featuresets): + """ + Train (fit) the scikit-learn estimator. + + :param labeled_featuresets: A list of ``(featureset, label)`` + where each ``featureset`` is a dict mapping strings to either + numbers, booleans or strings. + """ + + X, y = list(zip(*labeled_featuresets)) + X = self._vectorizer.fit_transform(X) + y = self._encoder.fit_transform(y) + self._clf.fit(X, y) + + return self + + def _make_probdist(self, y_proba): + classes = self._encoder.classes_ + return DictionaryProbDist({classes[i]: p for i, p in enumerate(y_proba)}) + + +if __name__ == "__main__": + from sklearn.linear_model import LogisticRegression + from sklearn.naive_bayes import BernoulliNB + + from nltk.classify.util import names_demo, names_demo_features + + # Bernoulli Naive Bayes is designed for binary classification. We set the + # binarize option to False since we know we're passing boolean features. + print("scikit-learn Naive Bayes:") + names_demo( + SklearnClassifier(BernoulliNB(binarize=False)).train, + features=names_demo_features, + ) + + # The C parameter on logistic regression (MaxEnt) controls regularization. + # The higher it's set, the less regularized the classifier is. + print("\n\nscikit-learn logistic regression:") + names_demo( + SklearnClassifier(LogisticRegression(C=1000)).train, + features=names_demo_features, + ) diff --git a/nltk/classify/senna.py b/nltk/classify/senna.py new file mode 100644 index 0000000..6fe001a --- /dev/null +++ b/nltk/classify/senna.py @@ -0,0 +1,189 @@ +# Natural Language Toolkit: Senna Interface +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Rami Al-Rfou' +# URL: +# For license information, see LICENSE.TXT + +""" +A general interface to the SENNA pipeline that supports any of the +operations specified in SUPPORTED_OPERATIONS. + +Applying multiple operations at once has the speed advantage. For example, +Senna will automatically determine POS tags if you are extracting named +entities. Applying both of the operations will cost only the time of +extracting the named entities. + +The SENNA pipeline has a fixed maximum size of the sentences that it can read. +By default it is 1024 token/sentence. If you have larger sentences, changing +the MAX_SENTENCE_SIZE value in SENNA_main.c should be considered and your +system specific binary should be rebuilt. Otherwise this could introduce +misalignment errors. + +The input is: + +- path to the directory that contains SENNA executables. If the path is incorrect, + Senna will automatically search for executable file specified in SENNA environment variable +- List of the operations needed to be performed. +- (optionally) the encoding of the input data (default:utf-8) + +Note: Unit tests for this module can be found in test/unit/test_senna.py + +>>> from nltk.classify import Senna +>>> pipeline = Senna('/usr/share/senna-v3.0', ['pos', 'chk', 'ner']) # doctest: +SKIP +>>> sent = 'Dusseldorf is an international business center'.split() +>>> [(token['word'], token['chk'], token['ner'], token['pos']) for token in pipeline.tag(sent)] # doctest: +SKIP +[('Dusseldorf', 'B-NP', 'B-LOC', 'NNP'), ('is', 'B-VP', 'O', 'VBZ'), ('an', 'B-NP', 'O', 'DT'), +('international', 'I-NP', 'O', 'JJ'), ('business', 'I-NP', 'O', 'NN'), ('center', 'I-NP', 'O', 'NN')] +""" + +from os import environ, path, sep +from platform import architecture, system +from subprocess import PIPE, Popen + +from nltk.tag.api import TaggerI + + +class Senna(TaggerI): + SUPPORTED_OPERATIONS = ["pos", "chk", "ner"] + + def __init__(self, senna_path, operations, encoding="utf-8"): + self._encoding = encoding + + # Only accept an explicit *absolute* senna_path as the location of the + # senna executable. A relative path (e.g. ".") must NOT be resolved + # against the current working directory: executable() returns a path + # that contains a separator (e.g. "./senna-osx"), and subprocess.Popen() + # runs such a path directly from the CWD without consulting $PATH, so an + # attacker who can write a "senna-" file there would have it + # executed -- running code loaded from an untrusted location (CWE-829; + # an untrusted search path, CWE-426/CWE-427). A relative path falls + # through to the trusted SENNA environment variable instead. + self._path = None + if path.isabs(senna_path): + self._path = path.normpath(senna_path) + sep + + # If the explicit (absolute) senna_path does not contain the executable, + # fall back to the SENNA environment variable, which must also be + # absolute for the same reason. + if self._path is None or not path.isfile(self.executable(self._path)): + senna_env = environ.get("SENNA") + if senna_env and path.isabs(senna_env): + self._path = path.normpath(senna_env) + sep + + # The executable must exist at this point; fail fast so construction is + # consistent (the path is verified here, not deferred to tag_sents()). + if self._path is None or not path.isfile(self.executable(self._path)): + raise LookupError( + "Senna executable not found. Pass an absolute senna_path to " + "Senna(...) or set the SENNA environment variable to the " + "absolute directory that contains the senna binary." + ) + + self.operations = operations + + def executable(self, base_path): + """ + The function that determines the system specific binary that should be + used in the pipeline. In case, the system is not known the default senna binary will + be used. + """ + os_name = system() + if os_name == "Linux": + bits = architecture()[0] + if bits == "64bit": + return path.join(base_path, "senna-linux64") + return path.join(base_path, "senna-linux32") + if os_name == "Windows": + return path.join(base_path, "senna-win32.exe") + if os_name == "Darwin": + return path.join(base_path, "senna-osx") + return path.join(base_path, "senna") + + def _map(self): + """ + A method that calculates the order of the columns that SENNA pipeline + will output the tags into. This depends on the operations being ordered. + """ + _map = {} + i = 1 + for operation in Senna.SUPPORTED_OPERATIONS: + if operation in self.operations: + _map[operation] = i + i += 1 + return _map + + def tag(self, tokens): + """ + Applies the specified operation(s) on a list of tokens. + """ + return self.tag_sents([tokens])[0] + + def tag_sents(self, sentences): + """ + Applies the tag method over a list of sentences. This method will return a + list of dictionaries. Every dictionary will contain a word with its + calculated annotations/tags. + """ + encoding = self._encoding + + if not path.isfile(self.executable(self._path)): + raise LookupError( + "Senna executable expected at %s but not found" + % self.executable(self._path) + ) + + # Build the senna command to run the tagger + _senna_cmd = [ + self.executable(self._path), + "-path", + self._path, + "-usrtokens", + "-iobtags", + ] + _senna_cmd.extend(["-" + op for op in self.operations]) + + # Serialize the actual sentences to a temporary string + _input = "\n".join(" ".join(x) for x in sentences) + "\n" + if isinstance(_input, str) and encoding: + _input = _input.encode(encoding) + + # Run the tagger and get the output + p = Popen(_senna_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + (stdout, stderr) = p.communicate(input=_input) + senna_output = stdout + + # Check the return code. + if p.returncode != 0: + raise RuntimeError("Senna command failed! Details: %s" % stderr) + + if encoding: + senna_output = stdout.decode(encoding) + + # Output the tagged sentences + map_ = self._map() + tagged_sentences = [[]] + sentence_index = 0 + token_index = 0 + for tagged_word in senna_output.strip().split("\n"): + if not tagged_word: + tagged_sentences.append([]) + sentence_index += 1 + token_index = 0 + continue + tags = tagged_word.split("\t") + result = {} + for tag in map_: + result[tag] = tags[map_[tag]].strip() + try: + result["word"] = sentences[sentence_index][token_index] + except IndexError as e: + raise IndexError( + "Misalignment error occurred at sentence number %d. Possible reason" + " is that the sentence size exceeded the maximum size. Check the " + "documentation of Senna class for more information." + % sentence_index + ) from e + tagged_sentences[-1].append(result) + token_index += 1 + return tagged_sentences diff --git a/nltk/classify/svm.py b/nltk/classify/svm.py new file mode 100644 index 0000000..66321c3 --- /dev/null +++ b/nltk/classify/svm.py @@ -0,0 +1,17 @@ +# Natural Language Toolkit: SVM-based classifier +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Leon Derczynski +# +# URL: +# For license information, see LICENSE.TXT +""" +nltk.classify.svm was deprecated. For classification based +on support vector machines SVMs use nltk.classify.scikitlearn +(or `scikit-learn `_ directly). +""" + + +class SvmClassifier: + def __init__(self, *args, **kwargs): + raise NotImplementedError(__doc__) diff --git a/nltk/classify/tadm.py b/nltk/classify/tadm.py new file mode 100644 index 0000000..4fa33aa --- /dev/null +++ b/nltk/classify/tadm.py @@ -0,0 +1,122 @@ +# Natural Language Toolkit: Interface to TADM Classifier +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Joseph Frazee +# URL: +# For license information, see LICENSE.TXT + +import subprocess +import sys + +from nltk.internals import find_binary + +try: + import numpy +except ImportError: + pass + +_tadm_bin = None + + +def config_tadm(bin=None): + global _tadm_bin + _tadm_bin = find_binary( + "tadm", bin, env_vars=["TADM"], binary_names=["tadm"], url="http://tadm.sf.net" + ) + + +def write_tadm_file(train_toks, encoding, stream): + """ + Generate an input file for ``tadm`` based on the given corpus of + classified tokens. + + :type train_toks: list(tuple(dict, str)) + :param train_toks: Training data, represented as a list of + pairs, the first member of which is a feature dictionary, + and the second of which is a classification label. + :type encoding: TadmEventMaxentFeatureEncoding + :param encoding: A feature encoding, used to convert featuresets + into feature vectors. + :type stream: stream + :param stream: The stream to which the ``tadm`` input file should be + written. + """ + # See the following for a file format description: + # + # https://sf.net/forum/forum.php?thread_id=1391502&forum_id=473054 + # https://sf.net/forum/forum.php?thread_id=1675097&forum_id=473054 + labels = encoding.labels() + for featureset, label in train_toks: + length_line = "%d\n" % len(labels) + stream.write(length_line) + for known_label in labels: + v = encoding.encode(featureset, known_label) + line = "%d %d %s\n" % ( + int(label == known_label), + len(v), + " ".join("%d %d" % u for u in v), + ) + stream.write(line) + + +def parse_tadm_weights(paramfile): + """ + Given the stdout output generated by ``tadm`` when training a + model, return a ``numpy`` array containing the corresponding weight + vector. + """ + weights = [] + for line in paramfile: + weights.append(float(line.strip())) + return numpy.array(weights, "d") + + +def call_tadm(args): + """ + Call the ``tadm`` binary with the given arguments. + """ + if isinstance(args, str): + raise TypeError("args should be a list of strings") + if _tadm_bin is None: + config_tadm() + + # Call tadm via a subprocess + cmd = [_tadm_bin] + args + p = subprocess.Popen(cmd, stdout=sys.stdout) + (stdout, stderr) = p.communicate() + + # Check the return code. + if p.returncode != 0: + print() + print(stderr) + raise OSError("tadm command failed!") + + +def names_demo(): + from nltk.classify.maxent import TadmMaxentClassifier + from nltk.classify.util import names_demo + + classifier = names_demo(TadmMaxentClassifier.train) + + +def encoding_demo(): + import sys + + from nltk.classify.maxent import TadmEventMaxentFeatureEncoding + + tokens = [ + ({"f0": 1, "f1": 1, "f3": 1}, "A"), + ({"f0": 1, "f2": 1, "f4": 1}, "B"), + ({"f0": 2, "f2": 1, "f3": 1, "f4": 1}, "A"), + ] + encoding = TadmEventMaxentFeatureEncoding.train(tokens) + write_tadm_file(tokens, encoding, sys.stdout) + print() + for i in range(encoding.length()): + print("%s --> %d" % (encoding.describe(i), i)) + print() + + +if __name__ == "__main__": + encoding_demo() + names_demo() diff --git a/nltk/classify/textcat.py b/nltk/classify/textcat.py new file mode 100644 index 0000000..c5f5ce6 --- /dev/null +++ b/nltk/classify/textcat.py @@ -0,0 +1,268 @@ +# Natural Language Toolkit: Language ID module using TextCat algorithm +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Avital Pekker +# +# URL: +# For license information, see LICENSE.TXT + +""" +A module for language identification using the TextCat algorithm. +An implementation of the text categorization algorithm +presented in Cavnar, W. B. and J. M. Trenkle, +"N-Gram-Based Text Categorization". + +The algorithm takes advantage of Zipf's law and uses +n-gram frequencies to profile languages and text-yet to +be identified-then compares using a distance measure. + +Language n-grams are provided by the "An Crubadan" +project. A corpus reader was created separately to read +those files. + +For details regarding the algorithm, see: +https://www.let.rug.nl/~vannoord/TextCat/textcat.pdf + +For details about An Crubadan, see: +https://borel.slu.edu/crubadan/index.html +""" + +from sys import maxsize + +from nltk.util import trigrams + +# Note: this is NOT "re" you're likely used to. The regex module +# is an alternative to the standard re module that supports +# Unicode codepoint properties with the \p{} syntax. +# You may have to "pip install regx" +try: + import regex as re +except ImportError: + re = None +###################################################################### +## Language identification using TextCat +###################################################################### + + +class TextCat: + _corpus = None + fingerprints = {} + _START_CHAR = "<" + _END_CHAR = ">" + + last_distances = {} + + def __init__(self): + if not re: + raise OSError( + "classify.textcat requires the regex module that " + "supports unicode. Try '$ pip install regex' and " + "see https://pypi.python.org/pypi/regex for " + "further details." + ) + + from nltk.corpus import crubadan + + self._corpus = crubadan + # Load all language ngrams into cache + for lang in self._corpus.langs(): + self._corpus.lang_freq(lang) + + def remove_punctuation(self, text): + """Get rid of punctuation except apostrophes""" + return re.sub(r"[^\P{P}\']+", "", text) + + def profile(self, text): + """Create FreqDist of trigrams within text""" + from nltk import FreqDist, word_tokenize + + clean_text = self.remove_punctuation(text) + tokens = word_tokenize(clean_text) + + fingerprint = FreqDist() + for t in tokens: + token_trigram_tuples = trigrams(self._START_CHAR + t + self._END_CHAR) + token_trigrams = ["".join(tri) for tri in token_trigram_tuples] + + for cur_trigram in token_trigrams: + if cur_trigram in fingerprint: + fingerprint[cur_trigram] += 1 + else: + fingerprint[cur_trigram] = 1 + + return fingerprint + + def calc_dist(self, lang, trigram, text_profile): + """Calculate the "out-of-place" measure between the + text and language profile for a single trigram""" + + lang_fd = self._corpus.lang_freq(lang) + dist = 0 + + if trigram in lang_fd: + idx_lang_profile = list(lang_fd.keys()).index(trigram) + idx_text = list(text_profile.keys()).index(trigram) + + # print(idx_lang_profile, ", ", idx_text) + dist = abs(idx_lang_profile - idx_text) + else: + # Arbitrary but should be larger than + # any possible trigram file length + # in terms of total lines + dist = maxsize + + return dist + + def lang_dists(self, text): + """Calculate the "out-of-place" measure between + the text and all languages""" + + distances = {} + profile = self.profile(text) + # For all the languages + for lang in self._corpus._all_lang_freq.keys(): + # Calculate distance metric for every trigram in + # input text to be identified + lang_dist = 0 + for trigram in profile: + lang_dist += self.calc_dist(lang, trigram, profile) + + distances[lang] = lang_dist + + return distances + + def guess_language(self, text, return_all=False): + """ + Determines the most likely language(s) for the given text. + + Parameters + ---------- + text : str + The text whose language is to be identified. + return_all : bool, optional + If False (default), returns a single ISO 639-3 language code as a str, + or None if the language is ambiguous or cannot be determined. + If True, returns a list of all language codes sharing the minimal distance. + The list will have one element if there is a unique best match, + multiple elements for ties, or be empty if no language is found. + + Returns + ------- + str or None, or list of str + If return_all is False: + - str: language code if unique minimum found + - None: if ambiguous or not classifiable + If return_all is True: + - list: possible language code(s), or empty list if not classifiable + + Examples + -------- + >>> from nltk.classify.textcat import TextCat + >>> cat = TextCat() + >>> print(cat.guess_language('The quick brown fox jumps over the lazy dog.')) + eng + + A case with no information, returns None or an empty list: + + >>> print(cat.guess_language('', return_all=True)) + [] + >>> print(cat.guess_language('')) + None + + A case where a single short input ties between Catalan and French: + + >>> print(sorted(cat.guess_language('ent', return_all=True))) + ['cat', 'fra'] + + By default (`return_all=False`), in a tie, guess_language returns None: + + >>> print(cat.guess_language('ent')) + None + + Note: For short or generic inputs, or for closely related languages, + the classifier may return an unexpected language. For example, + the following is a perfectly grammatical English sentence, but may + be classified as Scots ('sco') due to profile similarity: + + >>> print(cat.guess_language('This is a short English sentence.')) + sco + + This behavior is not a bug, but an artifact of the underlying n-gram profiles. + The classifier should be used with sufficiently distinctive and longer text fragments + for best accuracy. + """ + self.last_distances = self.lang_dists(text) + if not self.last_distances: + if return_all: + return [] + return None + min_dist = min(self.last_distances.values()) + candidates = [ + lang for lang, dist in self.last_distances.items() if dist == min_dist + ] + all_languages = list(self.last_distances.keys()) + + # Special case: all languages match equally (uninformative), return empty list/None + if len(candidates) == len(all_languages): + if return_all: + return [] + return None + + if return_all: + return candidates + if len(candidates) == 1: + return candidates[0] + return None + + +def demo(): + from nltk.corpus import udhr + + langs = [ + "Kurdish-UTF8", + "Abkhaz-UTF8", + "Farsi_Persian-UTF8", + "Hindi-UTF8", + "Hawaiian-UTF8", + "Russian-UTF8", + "Vietnamese-UTF8", + "Serbian_Srpski-UTF8", + "Esperanto-UTF8", + ] + + friendly = { + "kmr": "Northern Kurdish", + "abk": "Abkhazian", + "pes": "Iranian Persian", + "hin": "Hindi", + "haw": "Hawaiian", + "rus": "Russian", + "vie": "Vietnamese", + "srp": "Serbian", + "epo": "Esperanto", + } + + tc = TextCat() + + for cur_lang in langs: + # Get raw data from UDHR corpus + raw_sentences = udhr.sents(cur_lang) + rows = len(raw_sentences) - 1 + cols = list(map(len, raw_sentences)) + + sample = "" + + # Generate a sample text of the language + for i in range(0, rows): + cur_sent = " " + " ".join([raw_sentences[i][j] for j in range(0, cols[i])]) + sample += cur_sent + + # Try to detect what it is + print("Language snippet: " + sample[0:140] + "...") + guess = tc.guess_language(sample) + print(f"Language detection: {guess} ({friendly[guess]})") + print("#" * 140) + + +if __name__ == "__main__": + demo() diff --git a/nltk/classify/util.py b/nltk/classify/util.py new file mode 100644 index 0000000..8ce96ea --- /dev/null +++ b/nltk/classify/util.py @@ -0,0 +1,347 @@ +# Natural Language Toolkit: Classifier Utility Functions +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# URL: +# For license information, see LICENSE.TXT + +""" +Utility functions and classes for classifiers. +""" + +import math + +# from nltk.util import Deprecated +import nltk.classify.util # for accuracy & log_likelihood +from nltk.util import LazyMap + +###################################################################### +# { Helper Functions +###################################################################### + + +# alternative name possibility: 'map_featurefunc()'? +# alternative name possibility: 'detect_features()'? +# alternative name possibility: 'map_featuredetect()'? +# or.. just have users use LazyMap directly? +def apply_features(feature_func, toks, labeled=None): + """ + Use the ``LazyMap`` class to construct a lazy list-like + object that is analogous to ``map(feature_func, toks)``. In + particular, if ``labeled=False``, then the returned list-like + object's values are equal to:: + + [feature_func(tok) for tok in toks] + + If ``labeled=True``, then the returned list-like object's values + are equal to:: + + [(feature_func(tok), label) for (tok, label) in toks] + + The primary purpose of this function is to avoid the memory + overhead involved in storing all the featuresets for every token + in a corpus. Instead, these featuresets are constructed lazily, + as-needed. The reduction in memory overhead can be especially + significant when the underlying list of tokens is itself lazy (as + is the case with many corpus readers). + + :param feature_func: The function that will be applied to each + token. It should return a featureset -- i.e., a dict + mapping feature names to feature values. + :param toks: The list of tokens to which ``feature_func`` should be + applied. If ``labeled=True``, then the list elements will be + passed directly to ``feature_func()``. If ``labeled=False``, + then the list elements should be tuples ``(tok,label)``, and + ``tok`` will be passed to ``feature_func()``. + :param labeled: If true, then ``toks`` contains labeled tokens -- + i.e., tuples of the form ``(tok, label)``. (Default: + auto-detect based on types.) + """ + if labeled is None: + labeled = toks and isinstance(toks[0], (tuple, list)) + if labeled: + + def lazy_func(labeled_token): + return (feature_func(labeled_token[0]), labeled_token[1]) + + return LazyMap(lazy_func, toks) + else: + return LazyMap(feature_func, toks) + + +def attested_labels(tokens): + """ + :return: A list of all labels that are attested in the given list + of tokens. + :rtype: list of (immutable) + :param tokens: The list of classified tokens from which to extract + labels. A classified token has the form ``(token, label)``. + :type tokens: list + """ + return tuple({label for (tok, label) in tokens}) + + +def log_likelihood(classifier, gold): + results = classifier.prob_classify_many([fs for (fs, l) in gold]) + ll = [pdist.prob(l) for ((fs, l), pdist) in zip(gold, results)] + return math.log(sum(ll) / len(ll)) + + +def accuracy(classifier, gold): + results = classifier.classify_many([fs for (fs, l) in gold]) + correct = [l == r for ((fs, l), r) in zip(gold, results)] + if correct: + return sum(correct) / len(correct) + else: + return 0 + + +class CutoffChecker: + """ + A helper class that implements cutoff checks based on number of + iterations and log likelihood. + + Accuracy cutoffs are also implemented, but they're almost never + a good idea to use. + """ + + def __init__(self, cutoffs): + self.cutoffs = cutoffs.copy() + if "min_ll" in cutoffs: + cutoffs["min_ll"] = -abs(cutoffs["min_ll"]) + if "min_lldelta" in cutoffs: + cutoffs["min_lldelta"] = abs(cutoffs["min_lldelta"]) + self.ll = None + self.acc = None + self.iter = 1 + + def check(self, classifier, train_toks): + cutoffs = self.cutoffs + self.iter += 1 + if "max_iter" in cutoffs and self.iter >= cutoffs["max_iter"]: + return True # iteration cutoff. + + new_ll = nltk.classify.util.log_likelihood(classifier, train_toks) + if math.isnan(new_ll): + return True + + if "min_ll" in cutoffs or "min_lldelta" in cutoffs: + if "min_ll" in cutoffs and new_ll >= cutoffs["min_ll"]: + return True # log likelihood cutoff + if ( + "min_lldelta" in cutoffs + and self.ll + and ((new_ll - self.ll) <= abs(cutoffs["min_lldelta"])) + ): + return True # log likelihood delta cutoff + self.ll = new_ll + + if "max_acc" in cutoffs or "min_accdelta" in cutoffs: + new_acc = nltk.classify.util.log_likelihood(classifier, train_toks) + if "max_acc" in cutoffs and new_acc >= cutoffs["max_acc"]: + return True # log likelihood cutoff + if ( + "min_accdelta" in cutoffs + and self.acc + and ((new_acc - self.acc) <= abs(cutoffs["min_accdelta"])) + ): + return True # log likelihood delta cutoff + self.acc = new_acc + + return False # no cutoff reached. + + +###################################################################### +# { Demos +###################################################################### + + +def names_demo_features(name): + features = {} + features["alwayson"] = True + features["startswith"] = name[0].lower() + features["endswith"] = name[-1].lower() + for letter in "abcdefghijklmnopqrstuvwxyz": + features["count(%s)" % letter] = name.lower().count(letter) + features["has(%s)" % letter] = letter in name.lower() + return features + + +def binary_names_demo_features(name): + features = {} + features["alwayson"] = True + features["startswith(vowel)"] = name[0].lower() in "aeiouy" + features["endswith(vowel)"] = name[-1].lower() in "aeiouy" + for letter in "abcdefghijklmnopqrstuvwxyz": + features["count(%s)" % letter] = name.lower().count(letter) + features["has(%s)" % letter] = letter in name.lower() + features["startswith(%s)" % letter] = letter == name[0].lower() + features["endswith(%s)" % letter] = letter == name[-1].lower() + return features + + +def names_demo(trainer, features=names_demo_features): + import random + + from nltk.corpus import names + + # Construct a list of classified names, using the names corpus. + namelist = [(name, "male") for name in names.words("male.txt")] + [ + (name, "female") for name in names.words("female.txt") + ] + + # Randomly split the names into a test & train set. + random.seed(123456) + random.shuffle(namelist) + train = namelist[:5000] + test = namelist[5000:5500] + + # Train up a classifier. + print("Training classifier...") + classifier = trainer([(features(n), g) for (n, g) in train]) + + # Run the classifier on the test data. + print("Testing classifier...") + acc = accuracy(classifier, [(features(n), g) for (n, g) in test]) + print("Accuracy: %6.4f" % acc) + + # For classifiers that can find probabilities, show the log + # likelihood and some sample probability distributions. + try: + test_featuresets = [features(n) for (n, g) in test] + pdists = classifier.prob_classify_many(test_featuresets) + ll = [pdist.logprob(gold) for ((name, gold), pdist) in zip(test, pdists)] + print("Avg. log likelihood: %6.4f" % (sum(ll) / len(test))) + print() + print("Unseen Names P(Male) P(Female)\n" + "-" * 40) + for (name, gender), pdist in list(zip(test, pdists))[:5]: + if gender == "male": + fmt = " %-15s *%6.4f %6.4f" + else: + fmt = " %-15s %6.4f *%6.4f" + print(fmt % (name, pdist.prob("male"), pdist.prob("female"))) + except NotImplementedError: + pass + + # Return the classifier + return classifier + + +def partial_names_demo(trainer, features=names_demo_features): + import random + + from nltk.corpus import names + + male_names = names.words("male.txt") + female_names = names.words("female.txt") + + random.seed(654321) + random.shuffle(male_names) + random.shuffle(female_names) + + # Create a list of male names to be used as positive-labeled examples for training + positive = map(features, male_names[:2000]) + + # Create a list of male and female names to be used as unlabeled examples + unlabeled = map(features, male_names[2000:2500] + female_names[:500]) + + # Create a test set with correctly-labeled male and female names + test = [(name, True) for name in male_names[2500:2750]] + [ + (name, False) for name in female_names[500:750] + ] + + random.shuffle(test) + + # Train up a classifier. + print("Training classifier...") + classifier = trainer(positive, unlabeled) + + # Run the classifier on the test data. + print("Testing classifier...") + acc = accuracy(classifier, [(features(n), m) for (n, m) in test]) + print("Accuracy: %6.4f" % acc) + + # For classifiers that can find probabilities, show the log + # likelihood and some sample probability distributions. + try: + test_featuresets = [features(n) for (n, m) in test] + pdists = classifier.prob_classify_many(test_featuresets) + ll = [pdist.logprob(gold) for ((name, gold), pdist) in zip(test, pdists)] + print("Avg. log likelihood: %6.4f" % (sum(ll) / len(test))) + print() + print("Unseen Names P(Male) P(Female)\n" + "-" * 40) + for (name, is_male), pdist in zip(test, pdists)[:5]: + if is_male: + fmt = " %-15s *%6.4f %6.4f" + else: + fmt = " %-15s %6.4f *%6.4f" + print(fmt % (name, pdist.prob(True), pdist.prob(False))) + except NotImplementedError: + pass + + # Return the classifier + return classifier + + +_inst_cache = {} + + +def wsd_demo(trainer, word, features, n=1000): + import random + + from nltk.corpus import senseval + + # Get the instances. + print("Reading data...") + global _inst_cache + if word not in _inst_cache: + _inst_cache[word] = [(i, i.senses[0]) for i in senseval.instances(word)] + instances = _inst_cache[word][:] + if n > len(instances): + n = len(instances) + senses = list({l for (i, l) in instances}) + print(" Senses: " + " ".join(senses)) + + # Randomly split the names into a test & train set. + print("Splitting into test & train...") + random.seed(123456) + random.shuffle(instances) + train = instances[: int(0.8 * n)] + test = instances[int(0.8 * n) : n] + + # Train up a classifier. + print("Training classifier...") + classifier = trainer([(features(i), l) for (i, l) in train]) + + # Run the classifier on the test data. + print("Testing classifier...") + acc = accuracy(classifier, [(features(i), l) for (i, l) in test]) + print("Accuracy: %6.4f" % acc) + + # For classifiers that can find probabilities, show the log + # likelihood and some sample probability distributions. + try: + test_featuresets = [features(i) for (i, n) in test] + pdists = classifier.prob_classify_many(test_featuresets) + ll = [pdist.logprob(gold) for ((name, gold), pdist) in zip(test, pdists)] + print("Avg. log likelihood: %6.4f" % (sum(ll) / len(test))) + except NotImplementedError: + pass + + # Return the classifier + return classifier + + +def check_megam_config(): + """ + Checks whether the MEGAM binary is configured. + """ + try: + _megam_bin + except NameError as e: + err_msg = str( + "Please configure your megam binary first, e.g.\n" + ">>> nltk.config_megam('/usr/bin/local/megam')" + ) + raise NameError(err_msg) from e diff --git a/nltk/classify/weka.py b/nltk/classify/weka.py new file mode 100644 index 0000000..8d85aff --- /dev/null +++ b/nltk/classify/weka.py @@ -0,0 +1,383 @@ +# Natural Language Toolkit: Interface to Weka Classsifiers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Classifiers that make use of the external 'Weka' package. +""" + +import os +import re +import subprocess +import tempfile +import time +import zipfile +from sys import stdin + +from nltk.classify.api import ClassifierI +from nltk.internals import config_java, java +from nltk.probability import DictionaryProbDist + +_weka_classpath = None +# NB: the current working directory (".") is deliberately NOT searched. Picking +# up a ``weka.jar`` from the CWD would load and run Java classes from an +# unverified jar -- ``java -cp ./weka.jar weka.classifiers.bayes.NaiveBayes ...`` +# -- with no integrity check, so an attacker who can write ``./weka.jar`` gets +# code execution (CWE-494; reachable via an untrusted search path, CWE-426). Set +# the WEKAHOME environment variable or pass ``config_weka(classpath=...)`` to +# point at a trusted weka.jar. +_weka_search = [ + "/usr/share/weka", + "/usr/local/share/weka", + "/usr/lib/weka", + "/usr/local/lib/weka", +] + + +def config_weka(classpath=None): + global _weka_classpath + + # Make sure java's configured first. + config_java() + + if classpath is not None: + _weka_classpath = classpath + + if _weka_classpath is None: + searchpath = list(_weka_search) # copy; don't mutate the module global + if "WEKAHOME" in os.environ: + searchpath.insert(0, os.environ["WEKAHOME"]) + + for path in searchpath: + if os.path.exists(os.path.join(path, "weka.jar")): + _weka_classpath = os.path.join(path, "weka.jar") + version = _check_weka_version(_weka_classpath) + if version: + print(f"[Found Weka: {_weka_classpath} (version {version})]") + else: + print("[Found Weka: %s]" % _weka_classpath) + _check_weka_version(_weka_classpath) + + if _weka_classpath is None: + raise LookupError( + "Unable to find weka.jar! Use config_weka() " + "or set the WEKAHOME environment variable. " + "For more information about Weka, please see " + "https://www.cs.waikato.ac.nz/ml/weka/" + ) + + +def _check_weka_version(jar): + try: + zf = zipfile.ZipFile(jar) + except (SystemExit, KeyboardInterrupt): + raise + except Exception: + return None + try: + try: + return zf.read("weka/core/version.txt") + except KeyError: + return None + finally: + zf.close() + + +class WekaClassifier(ClassifierI): + def __init__(self, formatter, model_filename): + self._formatter = formatter + self._model = model_filename + + def prob_classify_many(self, featuresets): + return self._classify_many(featuresets, ["-p", "0", "-distribution"]) + + def classify_many(self, featuresets): + return self._classify_many(featuresets, ["-p", "0"]) + + def _classify_many(self, featuresets, options): + # Make sure we can find java & weka. + config_weka() + + temp_dir = tempfile.mkdtemp() + try: + # Write the test data file. + test_filename = os.path.join(temp_dir, "test.arff") + self._formatter.write(test_filename, featuresets) + + # Call weka to classify the data. + cmd = [ + "weka.classifiers.bayes.NaiveBayes", + "-l", + self._model, + "-T", + test_filename, + ] + options + (stdout, stderr) = java( + cmd, + classpath=_weka_classpath, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + # Check if something went wrong: + if stderr and not stdout: + if "Illegal options: -distribution" in stderr: + raise ValueError( + "The installed version of weka does " + "not support probability distribution " + "output." + ) + else: + raise ValueError("Weka failed to generate output:\n%s" % stderr) + + # Parse weka's output. + return self.parse_weka_output(stdout.decode(stdin.encoding).split("\n")) + + finally: + for f in os.listdir(temp_dir): + os.remove(os.path.join(temp_dir, f)) + os.rmdir(temp_dir) + + def parse_weka_distribution(self, s): + probs = [float(v) for v in re.split("[*,]+", s) if v.strip()] + probs = dict(zip(self._formatter.labels(), probs)) + return DictionaryProbDist(probs) + + def parse_weka_output(self, lines): + # Strip unwanted text from stdout + for i, line in enumerate(lines): + if line.strip().startswith("inst#"): + lines = lines[i:] + break + + if lines[0].split() == ["inst#", "actual", "predicted", "error", "prediction"]: + return [line.split()[2].split(":")[1] for line in lines[1:] if line.strip()] + elif lines[0].split() == [ + "inst#", + "actual", + "predicted", + "error", + "distribution", + ]: + return [ + self.parse_weka_distribution(line.split()[-1]) + for line in lines[1:] + if line.strip() + ] + + # is this safe:? + elif re.match(r"^0 \w+ [01]\.[0-9]* \?\s*$", lines[0]): + return [line.split()[1] for line in lines if line.strip()] + + else: + for line in lines[:10]: + print(line) + raise ValueError( + "Unhandled output format -- your version " + "of weka may not be supported.\n" + " Header: %s" % lines[0] + ) + + # [xx] full list of classifiers (some may be abstract?): + # ADTree, AODE, BayesNet, ComplementNaiveBayes, ConjunctiveRule, + # DecisionStump, DecisionTable, HyperPipes, IB1, IBk, Id3, J48, + # JRip, KStar, LBR, LeastMedSq, LinearRegression, LMT, Logistic, + # LogisticBase, M5Base, MultilayerPerceptron, + # MultipleClassifiersCombiner, NaiveBayes, NaiveBayesMultinomial, + # NaiveBayesSimple, NBTree, NNge, OneR, PaceRegression, PART, + # PreConstructedLinearModel, Prism, RandomForest, + # RandomizableClassifier, RandomTree, RBFNetwork, REPTree, Ridor, + # RuleNode, SimpleLinearRegression, SimpleLogistic, + # SingleClassifierEnhancer, SMO, SMOreg, UserClassifier, VFI, + # VotedPerceptron, Winnow, ZeroR + + _CLASSIFIER_CLASS = { + "naivebayes": "weka.classifiers.bayes.NaiveBayes", + "C4.5": "weka.classifiers.trees.J48", + "log_regression": "weka.classifiers.functions.Logistic", + "svm": "weka.classifiers.functions.SMO", + "kstar": "weka.classifiers.lazy.KStar", + "ripper": "weka.classifiers.rules.JRip", + } + + @classmethod + def train( + cls, + model_filename, + featuresets, + classifier="naivebayes", + options=[], + quiet=True, + ): + # Make sure we can find java & weka. + config_weka() + + # Build an ARFF formatter. + formatter = ARFF_Formatter.from_train(featuresets) + + temp_dir = tempfile.mkdtemp() + try: + # Write the training data file. + train_filename = os.path.join(temp_dir, "train.arff") + formatter.write(train_filename, featuresets) + + if classifier in cls._CLASSIFIER_CLASS: + javaclass = cls._CLASSIFIER_CLASS[classifier] + elif classifier in cls._CLASSIFIER_CLASS.values(): + javaclass = classifier + else: + raise ValueError("Unknown classifier %s" % classifier) + + # Train the weka model. + cmd = [javaclass, "-d", model_filename, "-t", train_filename] + cmd += list(options) + if quiet: + stdout = subprocess.PIPE + else: + stdout = None + java(cmd, classpath=_weka_classpath, stdout=stdout) + + # Return the new classifier. + return WekaClassifier(formatter, model_filename) + + finally: + for f in os.listdir(temp_dir): + os.remove(os.path.join(temp_dir, f)) + os.rmdir(temp_dir) + + +class ARFF_Formatter: + """ + Converts featuresets and labeled featuresets to ARFF-formatted + strings, appropriate for input into Weka. + + Features and classes can be specified manually in the constructor, or may + be determined from data using ``from_train``. + """ + + def __init__(self, labels, features): + """ + :param labels: A list of all class labels that can be generated. + :param features: A list of feature specifications, where + each feature specification is a tuple (fname, ftype); + and ftype is an ARFF type string such as NUMERIC or + STRING. + """ + self._labels = labels + self._features = features + + def format(self, tokens): + """Returns a string representation of ARFF output for the given data.""" + return self.header_section() + self.data_section(tokens) + + def labels(self): + """Returns the list of classes.""" + return list(self._labels) + + def write(self, outfile, tokens): + """Writes ARFF data to a file for the given data.""" + if not hasattr(outfile, "write"): + outfile = open(outfile, "w") + outfile.write(self.format(tokens)) + outfile.close() + + @staticmethod + def from_train(tokens): + """ + Constructs an ARFF_Formatter instance with class labels and feature + types determined from the given data. Handles boolean, numeric and + string (note: not nominal) types. + """ + # Find the set of all attested labels. + labels = {label for (tok, label) in tokens} + + # Determine the types of all features. + features = {} + for tok, label in tokens: + for fname, fval in tok.items(): + if issubclass(type(fval), bool): + ftype = "{True, False}" + elif issubclass(type(fval), (int, float, bool)): + ftype = "NUMERIC" + elif issubclass(type(fval), str): + ftype = "STRING" + elif fval is None: + continue # can't tell the type. + else: + raise ValueError("Unsupported value type %r" % ftype) + + if features.get(fname, ftype) != ftype: + raise ValueError("Inconsistent type for %s" % fname) + features[fname] = ftype + features = sorted(features.items()) + + return ARFF_Formatter(labels, features) + + def header_section(self): + """Returns an ARFF header as a string.""" + # Header comment. + s = ( + "% Weka ARFF file\n" + + "% Generated automatically by NLTK\n" + + "%% %s\n\n" % time.ctime() + ) + + # Relation name + s += "@RELATION rel\n\n" + + # Input attribute specifications + for fname, ftype in self._features: + s += "@ATTRIBUTE %-30r %s\n" % (fname, ftype) + + # Label attribute specification + s += "@ATTRIBUTE %-30r {%s}\n" % ("-label-", ",".join(self._labels)) + + return s + + def data_section(self, tokens, labeled=None): + """ + Returns the ARFF data section for the given data. + + :param tokens: a list of featuresets (dicts) or labelled featuresets + which are tuples (featureset, label). + :param labeled: Indicates whether the given tokens are labeled + or not. If None, then the tokens will be assumed to be + labeled if the first token's value is a tuple or list. + """ + # Check if the tokens are labeled or unlabeled. If unlabeled, + # then use 'None' + if labeled is None: + labeled = tokens and isinstance(tokens[0], (tuple, list)) + if not labeled: + tokens = [(tok, None) for tok in tokens] + + # Data section + s = "\n@DATA\n" + for tok, label in tokens: + for fname, ftype in self._features: + s += "%s," % self._fmt_arff_val(tok.get(fname)) + s += "%s\n" % self._fmt_arff_val(label) + + return s + + def _fmt_arff_val(self, fval): + if fval is None: + return "?" + elif isinstance(fval, (bool, int)): + return "%s" % fval + elif isinstance(fval, float): + return "%r" % fval + else: + return "%r" % fval + + +if __name__ == "__main__": + from nltk.classify.util import binary_names_demo_features, names_demo + + def make_classifier(featuresets): + return WekaClassifier.train("/tmp/name.model", featuresets, "C4.5") + + classifier = names_demo(make_classifier, binary_names_demo_features) diff --git a/nltk/cli.py b/nltk/cli.py new file mode 100644 index 0000000..7576a95 --- /dev/null +++ b/nltk/cli.py @@ -0,0 +1,55 @@ +# Natural Language Toolkit: NLTK Command-Line Interface +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + + +import click +from tqdm import tqdm + +from nltk import word_tokenize +from nltk.util import parallelize_preprocess + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + + +@click.group(context_settings=CONTEXT_SETTINGS) +@click.version_option() +def cli(): + pass + + +@cli.command("tokenize") +@click.option( + "--language", + "-l", + default="en", + help="The language for the Punkt sentence tokenization.", +) +@click.option( + "--preserve-line", + "-p", + default=True, + is_flag=True, + help="An option to keep the preserve the sentence and not sentence tokenize it.", +) +@click.option("--processes", "-j", default=1, help="No. of processes.") +@click.option("--encoding", "-e", default="utf8", help="Specify encoding of file.") +@click.option( + "--delimiter", "-d", default=" ", help="Specify delimiter to join the tokens." +) +def tokenize_file(language, preserve_line, processes, encoding, delimiter): + """This command tokenizes text stream using nltk.word_tokenize""" + with click.get_text_stream("stdin", encoding=encoding) as fin: + with click.get_text_stream("stdout", encoding=encoding) as fout: + # If it's single process, joblib parallelization is slower, + # so just process line by line normally. + if processes == 1: + for line in tqdm(fin.readlines()): + print(delimiter.join(word_tokenize(line)), end="\n", file=fout) + else: + for outline in parallelize_preprocess( + word_tokenize, fin.readlines(), processes, progress_bar=True + ): + print(delimiter.join(outline), end="\n", file=fout) diff --git a/nltk/cluster/__init__.py b/nltk/cluster/__init__.py new file mode 100644 index 0000000..900103e --- /dev/null +++ b/nltk/cluster/__init__.py @@ -0,0 +1,92 @@ +# Natural Language Toolkit: Clusterers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# URL: +# For license information, see LICENSE.TXT + +""" +This module contains a number of basic clustering algorithms. Clustering +describes the task of discovering groups of similar items with a large +collection. It is also describe as unsupervised machine learning, as the data +from which it learns is unannotated with class information, as is the case for +supervised learning. Annotated data is difficult and expensive to obtain in +the quantities required for the majority of supervised learning algorithms. +This problem, the knowledge acquisition bottleneck, is common to most natural +language processing tasks, thus fueling the need for quality unsupervised +approaches. + +This module contains a k-means clusterer, E-M clusterer and a group average +agglomerative clusterer (GAAC). All these clusterers involve finding good +cluster groupings for a set of vectors in multi-dimensional space. + +The K-means clusterer starts with k arbitrary chosen means then allocates each +vector to the cluster with the closest mean. It then recalculates the means of +each cluster as the centroid of the vectors in the cluster. This process +repeats until the cluster memberships stabilise. This is a hill-climbing +algorithm which may converge to a local maximum. Hence the clustering is +often repeated with random initial means and the most commonly occurring +output means are chosen. + +The GAAC clusterer starts with each of the *N* vectors as singleton clusters. +It then iteratively merges pairs of clusters which have the closest centroids. +This continues until there is only one cluster. The order of merges gives rise +to a dendrogram - a tree with the earlier merges lower than later merges. The +membership of a given number of clusters *c*, *1 <= c <= N*, can be found by +cutting the dendrogram at depth *c*. + +The Gaussian EM clusterer models the vectors as being produced by a mixture +of k Gaussian sources. The parameters of these sources (prior probability, +mean and covariance matrix) are then found to maximise the likelihood of the +given data. This is done with the expectation maximisation algorithm. It +starts with k arbitrarily chosen means, priors and covariance matrices. It +then calculates the membership probabilities for each vector in each of the +clusters - this is the 'E' step. The cluster parameters are then updated in +the 'M' step using the maximum likelihood estimate from the cluster membership +probabilities. This process continues until the likelihood of the data does +not significantly increase. + +They all extend the ClusterI interface which defines common operations +available with each clusterer. These operations include: + +- cluster: clusters a sequence of vectors +- classify: assign a vector to a cluster +- classification_probdist: give the probability distribution over cluster memberships + +The current existing classifiers also extend cluster.VectorSpace, an +abstract class which allows for singular value decomposition (SVD) and vector +normalisation. SVD is used to reduce the dimensionality of the vector space in +such a manner as to preserve as much of the variation as possible, by +reparameterising the axes in order of variability and discarding all bar the +first d dimensions. Normalisation ensures that vectors fall in the unit +hypersphere. + +Usage example (see also demo()):: + + from nltk import cluster + from nltk.cluster import euclidean_distance + from numpy import array + + vectors = [array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0]]] + + # initialise the clusterer (will also assign the vectors to clusters) + clusterer = cluster.KMeansClusterer(2, euclidean_distance) + clusterer.cluster(vectors, True) + + # classify a new vector + print(clusterer.classify(array([3, 3]))) + +Note that the vectors must use numpy array-like +objects. nltk_contrib.unimelb.tacohn.SparseArrays may be used for +efficiency when required. +""" + +from nltk.cluster.em import EMClusterer +from nltk.cluster.gaac import GAAClusterer +from nltk.cluster.kmeans import KMeansClusterer +from nltk.cluster.util import ( + Dendrogram, + VectorSpaceClusterer, + cosine_distance, + euclidean_distance, +) diff --git a/nltk/cluster/api.py b/nltk/cluster/api.py new file mode 100644 index 0000000..71a4695 --- /dev/null +++ b/nltk/cluster/api.py @@ -0,0 +1,74 @@ +# Natural Language Toolkit: Clusterer Interfaces +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Porting: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from abc import ABCMeta, abstractmethod + +from nltk.probability import DictionaryProbDist + + +class ClusterI(metaclass=ABCMeta): + """ + Interface covering basic clustering functionality. + """ + + @abstractmethod + def cluster(self, vectors, assign_clusters=False): + """ + Assigns the vectors to clusters, learning the clustering parameters + from the data. Returns a cluster identifier for each vector. + """ + + @abstractmethod + def classify(self, token): + """ + Classifies the token into a cluster, setting the token's CLUSTER + parameter to that cluster identifier. + """ + + def likelihood(self, vector, label): + """ + Returns the likelihood (a float) of the token having the + corresponding cluster. + """ + if self.classify(vector) == label: + return 1.0 + else: + return 0.0 + + def classification_probdist(self, vector): + """ + Classifies the token into a cluster, returning + a probability distribution over the cluster identifiers. + """ + likelihoods = {} + sum = 0.0 + for cluster in self.cluster_names(): + likelihoods[cluster] = self.likelihood(vector, cluster) + sum += likelihoods[cluster] + for cluster in self.cluster_names(): + likelihoods[cluster] /= sum + return DictionaryProbDist(likelihoods) + + @abstractmethod + def num_clusters(self): + """ + Returns the number of clusters. + """ + + def cluster_names(self): + """ + Returns the names of the clusters. + :rtype: list + """ + return list(range(self.num_clusters())) + + def cluster_name(self, index): + """ + Returns the names of the cluster at index. + """ + return index diff --git a/nltk/cluster/em.py b/nltk/cluster/em.py new file mode 100644 index 0000000..5d9709b --- /dev/null +++ b/nltk/cluster/em.py @@ -0,0 +1,219 @@ +# Natural Language Toolkit: Expectation Maximization Clusterer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# URL: +# For license information, see LICENSE.TXT + +try: + import numpy +except ImportError: + pass + +from nltk.cluster.util import VectorSpaceClusterer + + +class EMClusterer(VectorSpaceClusterer): + """ + The Gaussian EM clusterer models the vectors as being produced by + a mixture of k Gaussian sources. The parameters of these sources + (prior probability, mean and covariance matrix) are then found to + maximise the likelihood of the given data. This is done with the + expectation maximisation algorithm. It starts with k arbitrarily + chosen means, priors and covariance matrices. It then calculates + the membership probabilities for each vector in each of the + clusters; this is the 'E' step. The cluster parameters are then + updated in the 'M' step using the maximum likelihood estimate from + the cluster membership probabilities. This process continues until + the likelihood of the data does not significantly increase. + """ + + def __init__( + self, + initial_means, + priors=None, + covariance_matrices=None, + conv_threshold=1e-6, + bias=0.1, + normalise=False, + svd_dimensions=None, + ): + """ + Creates an EM clusterer with the given starting parameters, + convergence threshold and vector mangling parameters. + + :param initial_means: the means of the gaussian cluster centers + :type initial_means: [seq of] numpy array or seq of SparseArray + :param priors: the prior probability for each cluster + :type priors: numpy array or seq of float + :param covariance_matrices: the covariance matrix for each cluster + :type covariance_matrices: [seq of] numpy array + :param conv_threshold: maximum change in likelihood before deemed + convergent + :type conv_threshold: int or float + :param bias: variance bias used to ensure non-singular covariance + matrices + :type bias: float + :param normalise: should vectors be normalised to length 1 + :type normalise: boolean + :param svd_dimensions: number of dimensions to use in reducing vector + dimensionsionality with SVD + :type svd_dimensions: int + """ + VectorSpaceClusterer.__init__(self, normalise, svd_dimensions) + self._means = numpy.array(initial_means, numpy.float64) + self._num_clusters = len(initial_means) + self._conv_threshold = conv_threshold + self._covariance_matrices = covariance_matrices + self._priors = priors + self._bias = bias + + def num_clusters(self): + return self._num_clusters + + def cluster_vectorspace(self, vectors, trace=False): + assert len(vectors) > 0 + + # set the parameters to initial values + dimensions = len(vectors[0]) + means = self._means + priors = self._priors + if not priors: + priors = self._priors = ( + numpy.ones(self._num_clusters, numpy.float64) / self._num_clusters + ) + covariances = self._covariance_matrices + if not covariances: + covariances = self._covariance_matrices = [ + numpy.identity(dimensions, numpy.float64) + for i in range(self._num_clusters) + ] + + # do the E and M steps until the likelihood plateaus + lastl = self._loglikelihood(vectors, priors, means, covariances) + converged = False + + while not converged: + if trace: + print("iteration; loglikelihood", lastl) + # E-step, calculate hidden variables, h[i,j] + h = numpy.zeros((len(vectors), self._num_clusters), numpy.float64) + for i in range(len(vectors)): + for j in range(self._num_clusters): + h[i, j] = priors[j] * self._gaussian( + means[j], covariances[j], vectors[i] + ) + h[i, :] /= sum(h[i, :]) + + # M-step, update parameters - cvm, p, mean + for j in range(self._num_clusters): + covariance_before = covariances[j] + new_covariance = numpy.zeros((dimensions, dimensions), numpy.float64) + new_mean = numpy.zeros(dimensions, numpy.float64) + sum_hj = 0.0 + for i in range(len(vectors)): + delta = vectors[i] - means[j] + new_covariance += h[i, j] * numpy.multiply.outer(delta, delta) + sum_hj += h[i, j] + new_mean += h[i, j] * vectors[i] + covariances[j] = new_covariance / sum_hj + means[j] = new_mean / sum_hj + priors[j] = sum_hj / len(vectors) + + # bias term to stop covariance matrix being singular + covariances[j] += self._bias * numpy.identity(dimensions, numpy.float64) + + # calculate likelihood - FIXME: may be broken + l = self._loglikelihood(vectors, priors, means, covariances) + + # check for convergence + if abs(lastl - l) < self._conv_threshold: + converged = True + lastl = l + + def classify_vectorspace(self, vector): + best = None + for j in range(self._num_clusters): + p = self._priors[j] * self._gaussian( + self._means[j], self._covariance_matrices[j], vector + ) + if not best or p > best[0]: + best = (p, j) + return best[1] + + def likelihood_vectorspace(self, vector, cluster): + cid = self.cluster_names().index(cluster) + return self._priors[cluster] * self._gaussian( + self._means[cluster], self._covariance_matrices[cluster], vector + ) + + def _gaussian(self, mean, cvm, x): + m = len(mean) + assert cvm.shape == (m, m), "bad sized covariance matrix, %s" % str(cvm.shape) + try: + det = numpy.linalg.det(cvm) + inv = numpy.linalg.inv(cvm) + a = det**-0.5 * (2 * numpy.pi) ** (-m / 2.0) + dx = x - mean + print(dx, inv) + b = -0.5 * numpy.dot(numpy.dot(dx, inv), dx) + return a * numpy.exp(b) + except OverflowError: + # happens when the exponent is negative infinity - i.e. b = 0 + # i.e. the inverse of cvm is huge (cvm is almost zero) + return 0 + + def _loglikelihood(self, vectors, priors, means, covariances): + llh = 0.0 + for vector in vectors: + p = 0 + for j in range(len(priors)): + p += priors[j] * self._gaussian(means[j], covariances[j], vector) + llh += numpy.log(p) + return llh + + def __repr__(self): + return "" % list(self._means) + + +def demo(): + """ + Non-interactive demonstration of the clusterers with simple 2-D data. + """ + + from nltk import cluster + + # example from figure 14.10, page 519, Manning and Schutze + + vectors = [numpy.array(f) for f in [[0.5, 0.5], [1.5, 0.5], [1, 3]]] + means = [[4, 2], [4, 2.01]] + + clusterer = cluster.EMClusterer(means, bias=0.1) + clusters = clusterer.cluster(vectors, True, trace=True) + + print("Clustered:", vectors) + print("As: ", clusters) + print() + + for c in range(2): + print("Cluster:", c) + print("Prior: ", clusterer._priors[c]) + print("Mean: ", clusterer._means[c]) + print("Covar: ", clusterer._covariance_matrices[c]) + print() + + # classify a new vector + vector = numpy.array([2, 2]) + print("classify(%s):" % vector, end=" ") + print(clusterer.classify(vector)) + + # show the classification probabilities + vector = numpy.array([2, 2]) + print("classification_probdist(%s):" % vector) + pdist = clusterer.classification_probdist(vector) + for sample in pdist.samples(): + print(f"{sample} => {pdist.prob(sample) * 100:.0f}%") + + +if __name__ == "__main__": + demo() diff --git a/nltk/cluster/gaac.py b/nltk/cluster/gaac.py new file mode 100644 index 0000000..a9f59fd --- /dev/null +++ b/nltk/cluster/gaac.py @@ -0,0 +1,170 @@ +# Natural Language Toolkit: Group Average Agglomerative Clusterer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# URL: +# For license information, see LICENSE.TXT + +try: + import numpy +except ImportError: + pass + +from nltk.cluster.util import Dendrogram, VectorSpaceClusterer, cosine_distance + + +class GAAClusterer(VectorSpaceClusterer): + """ + The Group Average Agglomerative starts with each of the N vectors as singleton + clusters. It then iteratively merges pairs of clusters which have the + closest centroids. This continues until there is only one cluster. The + order of merges gives rise to a dendrogram: a tree with the earlier merges + lower than later merges. The membership of a given number of clusters c, 1 + <= c <= N, can be found by cutting the dendrogram at depth c. + + This clusterer uses the cosine similarity metric only, which allows for + efficient speed-up in the clustering process. + """ + + def __init__(self, num_clusters=1, normalise=True, svd_dimensions=None): + VectorSpaceClusterer.__init__(self, normalise, svd_dimensions) + self._num_clusters = num_clusters + self._dendrogram = None + self._groups_values = None + + def cluster(self, vectors, assign_clusters=False, trace=False): + # stores the merge order + self._dendrogram = Dendrogram( + [numpy.array(vector, numpy.float64) for vector in vectors] + ) + return VectorSpaceClusterer.cluster(self, vectors, assign_clusters, trace) + + def cluster_vectorspace(self, vectors, trace=False): + # variables describing the initial situation + N = len(vectors) + cluster_len = [1] * N + cluster_count = N + index_map = numpy.arange(N) + + # construct the similarity matrix + dims = (N, N) + dist = numpy.ones(dims, dtype=float) * numpy.inf + for i in range(N): + for j in range(i + 1, N): + dist[i, j] = cosine_distance(vectors[i], vectors[j]) + + while cluster_count > max(self._num_clusters, 1): + i, j = numpy.unravel_index(dist.argmin(), dims) + if trace: + print("merging %d and %d" % (i, j)) + + # update similarities for merging i and j + self._merge_similarities(dist, cluster_len, i, j) + + # remove j + dist[:, j] = numpy.inf + dist[j, :] = numpy.inf + + # merge the clusters + cluster_len[i] = cluster_len[i] + cluster_len[j] + self._dendrogram.merge(index_map[i], index_map[j]) + cluster_count -= 1 + + # update the index map to reflect the indexes if we + # had removed j + index_map[j + 1 :] -= 1 + index_map[j] = N + + self.update_clusters(self._num_clusters) + + def _merge_similarities(self, dist, cluster_len, i, j): + # the new cluster i merged from i and j adopts the average of + # i and j's similarity to each other cluster, weighted by the + # number of points in the clusters i and j + i_weight = cluster_len[i] + j_weight = cluster_len[j] + weight_sum = i_weight + j_weight + + # update for x 0 + if self._should_normalise: + centroid = self._normalise(cluster[0]) + else: + centroid = numpy.array(cluster[0]) + for vector in cluster[1:]: + if self._should_normalise: + centroid += self._normalise(vector) + else: + centroid += vector + centroid /= len(cluster) + self._centroids.append(centroid) + self._num_clusters = len(self._centroids) + + def classify_vectorspace(self, vector): + best = None + for i in range(self._num_clusters): + centroid = self._centroids[i] + dist = cosine_distance(vector, centroid) + if not best or dist < best[0]: + best = (dist, i) + return best[1] + + def dendrogram(self): + """ + :return: The dendrogram representing the current clustering + :rtype: Dendrogram + """ + return self._dendrogram + + def num_clusters(self): + return self._num_clusters + + def __repr__(self): + return "" % self._num_clusters + + +def demo(): + """ + Non-interactive demonstration of the clusterers with simple 2-D data. + """ + + from nltk.cluster import GAAClusterer + + # use a set of tokens with 2D indices + vectors = [numpy.array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0], [2, 3], [3, 1]]] + + # test the GAAC clusterer with 4 clusters + clusterer = GAAClusterer(4) + clusters = clusterer.cluster(vectors, True) + + print("Clusterer:", clusterer) + print("Clustered:", vectors) + print("As:", clusters) + print() + + # show the dendrogram + clusterer.dendrogram().show() + + # classify a new vector + vector = numpy.array([3, 3]) + print("classify(%s):" % vector, end=" ") + print(clusterer.classify(vector)) + print() + + +if __name__ == "__main__": + demo() diff --git a/nltk/cluster/kmeans.py b/nltk/cluster/kmeans.py new file mode 100644 index 0000000..97b1981 --- /dev/null +++ b/nltk/cluster/kmeans.py @@ -0,0 +1,230 @@ +# Natural Language Toolkit: K-Means Clusterer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# URL: +# For license information, see LICENSE.TXT + +import copy +import random +import sys + +try: + import numpy +except ImportError: + pass + + +from nltk.cluster.util import VectorSpaceClusterer + + +class KMeansClusterer(VectorSpaceClusterer): + """ + The K-means clusterer starts with k arbitrary chosen means then allocates + each vector to the cluster with the closest mean. It then recalculates the + means of each cluster as the centroid of the vectors in the cluster. This + process repeats until the cluster memberships stabilise. This is a + hill-climbing algorithm which may converge to a local maximum. Hence the + clustering is often repeated with random initial means and the most + commonly occurring output means are chosen. + """ + + def __init__( + self, + num_means, + distance, + repeats=1, + conv_test=1e-6, + initial_means=None, + normalise=False, + svd_dimensions=None, + rng=None, + avoid_empty_clusters=False, + ): + """ + :param num_means: the number of means to use (may use fewer) + :type num_means: int + :param distance: measure of distance between two vectors + :type distance: function taking two vectors and returning a float + :param repeats: number of randomised clustering trials to use + :type repeats: int + :param conv_test: maximum variation in mean differences before + deemed convergent + :type conv_test: number + :param initial_means: set of k initial means + :type initial_means: sequence of vectors + :param normalise: should vectors be normalised to length 1 + :type normalise: boolean + :param svd_dimensions: number of dimensions to use in reducing vector + dimensionsionality with SVD + :type svd_dimensions: int + :param rng: random number generator (or None) + :type rng: Random + :param avoid_empty_clusters: include current centroid in computation + of next one; avoids undefined behavior + when clusters become empty + :type avoid_empty_clusters: boolean + """ + VectorSpaceClusterer.__init__(self, normalise, svd_dimensions) + self._num_means = num_means + self._distance = distance + self._max_difference = conv_test + assert not initial_means or len(initial_means) == num_means + self._means = initial_means + assert repeats >= 1 + assert not (initial_means and repeats > 1) + self._repeats = repeats + self._rng = rng if rng else random.Random() + self._avoid_empty_clusters = avoid_empty_clusters + + def cluster_vectorspace(self, vectors, trace=False): + if self._means and self._repeats > 1: + print("Warning: means will be discarded for subsequent trials") + + meanss = [] + for trial in range(self._repeats): + if trace: + print("k-means trial", trial) + if not self._means or trial > 1: + self._means = self._rng.sample(list(vectors), self._num_means) + self._cluster_vectorspace(vectors, trace) + meanss.append(self._means) + + if len(meanss) > 1: + # sort the means first (so that different cluster numbering won't + # effect the distance comparison) + for means in meanss: + means.sort(key=sum) + + # find the set of means that's minimally different from the others + min_difference = min_means = None + for i in range(len(meanss)): + d = 0 + for j in range(len(meanss)): + if i != j: + d += self._sum_distances(meanss[i], meanss[j]) + if min_difference is None or d < min_difference: + min_difference, min_means = d, meanss[i] + + # use the best means + self._means = min_means + + def _cluster_vectorspace(self, vectors, trace=False): + if self._num_means < len(vectors): + # perform k-means clustering + converged = False + while not converged: + # assign the tokens to clusters based on minimum distance to + # the cluster means + clusters = [[] for m in range(self._num_means)] + for vector in vectors: + index = self.classify_vectorspace(vector) + clusters[index].append(vector) + + if trace: + print("iteration") + # for i in range(self._num_means): + # print ' mean', i, 'allocated', len(clusters[i]), 'vectors' + + # recalculate cluster means by computing the centroid of each cluster + new_means = list(map(self._centroid, clusters, self._means)) + + # measure the degree of change from the previous step for convergence + difference = self._sum_distances(self._means, new_means) + if difference < self._max_difference: + converged = True + + # remember the new means + self._means = new_means + + def classify_vectorspace(self, vector): + # finds the closest cluster centroid + # returns that cluster's index + best_distance = best_index = None + for index in range(len(self._means)): + mean = self._means[index] + dist = self._distance(vector, mean) + if best_distance is None or dist < best_distance: + best_index, best_distance = index, dist + return best_index + + def num_clusters(self): + if self._means: + return len(self._means) + else: + return self._num_means + + def means(self): + """ + The means used for clustering. + """ + return self._means + + def _sum_distances(self, vectors1, vectors2): + difference = 0.0 + for u, v in zip(vectors1, vectors2): + difference += self._distance(u, v) + return difference + + def _centroid(self, cluster, mean): + if self._avoid_empty_clusters: + centroid = copy.copy(mean) + for vector in cluster: + centroid += vector + return centroid / (1 + len(cluster)) + else: + if not len(cluster): + sys.stderr.write("Error: no centroid defined for empty cluster.\n") + sys.stderr.write( + "Try setting argument 'avoid_empty_clusters' to True\n" + ) + assert False + centroid = copy.copy(cluster[0]) + for vector in cluster[1:]: + centroid += vector + return centroid / len(cluster) + + def __repr__(self): + return "" % (self._means, self._repeats) + + +################################################################################# + + +def demo(): + # example from figure 14.9, page 517, Manning and Schutze + + from nltk.cluster import KMeansClusterer, euclidean_distance + + vectors = [numpy.array(f) for f in [[2, 1], [1, 3], [4, 7], [6, 7]]] + means = [[4, 3], [5, 5]] + + clusterer = KMeansClusterer(2, euclidean_distance, initial_means=means) + clusters = clusterer.cluster(vectors, True, trace=True) + + print("Clustered:", vectors) + print("As:", clusters) + print("Means:", clusterer.means()) + print() + + vectors = [numpy.array(f) for f in [[3, 3], [1, 2], [4, 2], [4, 0], [2, 3], [3, 1]]] + + # test k-means using the euclidean distance metric, 2 means and repeat + # clustering 10 times with random seeds + + clusterer = KMeansClusterer(2, euclidean_distance, repeats=10) + clusters = clusterer.cluster(vectors, True) + print("Clustered:", vectors) + print("As:", clusters) + print("Means:", clusterer.means()) + print() + + # classify a new vector + vector = numpy.array([3, 3]) + print("classify(%s):" % vector, end=" ") + print(clusterer.classify(vector)) + print() + + +if __name__ == "__main__": + demo() diff --git a/nltk/cluster/util.py b/nltk/cluster/util.py new file mode 100644 index 0000000..116936e --- /dev/null +++ b/nltk/cluster/util.py @@ -0,0 +1,300 @@ +# Natural Language Toolkit: Clusterer Utilities +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Contributor: J Richard Snape +# URL: +# For license information, see LICENSE.TXT +import copy +from abc import abstractmethod +from math import sqrt +from sys import stdout + +try: + import numpy +except ImportError: + pass + +from nltk.cluster.api import ClusterI + + +class VectorSpaceClusterer(ClusterI): + """ + Abstract clusterer which takes tokens and maps them into a vector space. + Optionally performs singular value decomposition to reduce the + dimensionality. + """ + + def __init__(self, normalise=False, svd_dimensions=None): + """ + :param normalise: should vectors be normalised to length 1 + :type normalise: boolean + :param svd_dimensions: number of dimensions to use in reducing vector + dimensionsionality with SVD + :type svd_dimensions: int + """ + self._Tt = None + self._should_normalise = normalise + self._svd_dimensions = svd_dimensions + + def cluster(self, vectors, assign_clusters=False, trace=False): + assert len(vectors) > 0 + + # normalise the vectors + if self._should_normalise: + vectors = list(map(self._normalise, vectors)) + + # use SVD to reduce the dimensionality + if self._svd_dimensions and self._svd_dimensions < len(vectors[0]): + [u, d, vt] = numpy.linalg.svd(numpy.transpose(numpy.array(vectors))) + S = d[: self._svd_dimensions] * numpy.identity( + self._svd_dimensions, numpy.float64 + ) + T = u[:, : self._svd_dimensions] + Dt = vt[: self._svd_dimensions, :] + vectors = numpy.transpose(numpy.dot(S, Dt)) + self._Tt = numpy.transpose(T) + + # call abstract method to cluster the vectors + self.cluster_vectorspace(vectors, trace) + + # assign the vectors to clusters + if assign_clusters: + return [self.classify(vector) for vector in vectors] + + @abstractmethod + def cluster_vectorspace(self, vectors, trace): + """ + Finds the clusters using the given set of vectors. + """ + + def classify(self, vector): + if self._should_normalise: + vector = self._normalise(vector) + if self._Tt is not None: + vector = numpy.dot(self._Tt, vector) + cluster = self.classify_vectorspace(vector) + return self.cluster_name(cluster) + + @abstractmethod + def classify_vectorspace(self, vector): + """ + Returns the index of the appropriate cluster for the vector. + """ + + def likelihood(self, vector, label): + if self._should_normalise: + vector = self._normalise(vector) + if self._Tt is not None: + vector = numpy.dot(self._Tt, vector) + return self.likelihood_vectorspace(vector, label) + + def likelihood_vectorspace(self, vector, cluster): + """ + Returns the likelihood of the vector belonging to the cluster. + """ + predicted = self.classify_vectorspace(vector) + return 1.0 if cluster == predicted else 0.0 + + def vector(self, vector): + """ + Returns the vector after normalisation and dimensionality reduction + """ + if self._should_normalise: + vector = self._normalise(vector) + if self._Tt is not None: + vector = numpy.dot(self._Tt, vector) + return vector + + def _normalise(self, vector): + """ + Normalises the vector to unit length. + """ + return vector / sqrt(numpy.dot(vector, vector)) + + +def euclidean_distance(u, v): + """ + Returns the euclidean distance between vectors u and v. This is equivalent + to the length of the vector (u - v). + """ + diff = u - v + return sqrt(numpy.dot(diff, diff)) + + +def cosine_distance(u, v): + """ + Returns 1 minus the cosine of the angle between vectors v and u. This is + equal to ``1 - (u.v / |u||v|)``. + """ + return 1 - (numpy.dot(u, v) / (sqrt(numpy.dot(u, u)) * sqrt(numpy.dot(v, v)))) + + +class _DendrogramNode: + """Tree node of a dendrogram.""" + + def __init__(self, value, *children): + self._value = value + self._children = children + + def leaves(self, values=True): + if self._children: + leaves = [] + for child in self._children: + leaves.extend(child.leaves(values)) + return leaves + elif values: + return [self._value] + else: + return [self] + + def groups(self, n): + queue = [(self._value, self)] + + while len(queue) < n: + priority, node = queue.pop() + if not node._children: + queue.push((priority, node)) + break + for child in node._children: + if child._children: + queue.append((child._value, child)) + else: + queue.append((0, child)) + # makes the earliest merges at the start, latest at the end + queue.sort() + + groups = [] + for priority, node in queue: + groups.append(node.leaves()) + return groups + + def __lt__(self, comparator): + return cosine_distance(self._value, comparator._value) < 0 + + +class Dendrogram: + """ + Represents a dendrogram, a tree with a specified branching order. This + must be initialised with the leaf items, then iteratively call merge for + each branch. This class constructs a tree representing the order of calls + to the merge function. + """ + + def __init__(self, items=[]): + """ + :param items: the items at the leaves of the dendrogram + :type items: sequence of (any) + """ + self._items = [_DendrogramNode(item) for item in items] + self._original_items = copy.copy(self._items) + self._merge = 1 + + def merge(self, *indices): + """ + Merges nodes at given indices in the dendrogram. The nodes will be + combined which then replaces the first node specified. All other nodes + involved in the merge will be removed. + + :param indices: indices of the items to merge (at least two) + :type indices: seq of int + """ + assert len(indices) >= 2 + node = _DendrogramNode(self._merge, *(self._items[i] for i in indices)) + self._merge += 1 + self._items[indices[0]] = node + for i in indices[1:]: + del self._items[i] + + def groups(self, n): + """ + Finds the n-groups of items (leaves) reachable from a cut at depth n. + :param n: number of groups + :type n: int + """ + if len(self._items) > 1: + root = _DendrogramNode(self._merge, *self._items) + else: + root = self._items[0] + return root.groups(n) + + def show(self, leaf_labels=[]): + """ + Print the dendrogram in ASCII art to standard out. + + :param leaf_labels: an optional list of strings to use for labeling the + leaves + :type leaf_labels: list + """ + + # ASCII rendering characters + JOIN, HLINK, VLINK = "+", "-", "|" + + # find the root (or create one) + if len(self._items) > 1: + root = _DendrogramNode(self._merge, *self._items) + else: + root = self._items[0] + leaves = self._original_items + + if leaf_labels: + last_row = leaf_labels + else: + last_row = ["%s" % leaf._value for leaf in leaves] + + # find the bottom row and the best cell width + width = max(map(len, last_row)) + 1 + lhalf = width // 2 + rhalf = int(width - lhalf - 1) + + # display functions + def format(centre, left=" ", right=" "): + return f"{lhalf * left}{centre}{right * rhalf}" + + def display(str): + stdout.write(str) + + # for each merge, top down + queue = [(root._value, root)] + verticals = [format(" ") for leaf in leaves] + while queue: + priority, node = queue.pop() + child_left_leaf = list(map(lambda c: c.leaves(False)[0], node._children)) + indices = list(map(leaves.index, child_left_leaf)) + if child_left_leaf: + min_idx = min(indices) + max_idx = max(indices) + for i in range(len(leaves)): + if leaves[i] in child_left_leaf: + if i == min_idx: + display(format(JOIN, " ", HLINK)) + elif i == max_idx: + display(format(JOIN, HLINK, " ")) + else: + display(format(JOIN, HLINK, HLINK)) + verticals[i] = format(VLINK) + elif min_idx <= i <= max_idx: + display(format(HLINK, HLINK, HLINK)) + else: + display(verticals[i]) + display("\n") + for child in node._children: + if child._children: + queue.append((child._value, child)) + queue.sort() + + for vertical in verticals: + display(vertical) + display("\n") + + # finally, display the last line + display("".join(item.center(width) for item in last_row)) + display("\n") + + def __repr__(self): + if len(self._items) > 1: + root = _DendrogramNode(self._merge, *self._items) + else: + root = self._items[0] + leaves = root.leaves(False) + return "" % len(leaves) diff --git a/nltk/collections.py b/nltk/collections.py new file mode 100644 index 0000000..317ce4a --- /dev/null +++ b/nltk/collections.py @@ -0,0 +1,657 @@ +# Natural Language Toolkit: Collections +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +import bisect +from functools import total_ordering +from itertools import chain, islice + +from nltk.internals import raise_unorderable_types, slice_bounds + +########################################################################## +# Ordered Dictionary +########################################################################## + + +class OrderedDict(dict): + def __init__(self, data=None, **kwargs): + self._keys = self.keys(data, kwargs.get("keys")) + self._default_factory = kwargs.get("default_factory") + if data is None: + dict.__init__(self) + else: + dict.__init__(self, data) + + def __delitem__(self, key): + dict.__delitem__(self, key) + self._keys.remove(key) + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + return self.__missing__(key) + + def __iter__(self): + return (key for key in self.keys()) + + def __missing__(self, key): + if not self._default_factory and key not in self._keys: + raise KeyError() + return self._default_factory() + + def __setitem__(self, key, item): + dict.__setitem__(self, key, item) + if key not in self._keys: + self._keys.append(key) + + def clear(self): + dict.clear(self) + self._keys.clear() + + def copy(self): + d = dict.copy(self) + d._keys = self._keys + return d + + def items(self): + return zip(self.keys(), self.values()) + + def keys(self, data=None, keys=None): + if data: + if keys: + assert isinstance(keys, list) + assert len(data) == len(keys) + return keys + else: + assert ( + isinstance(data, dict) + or isinstance(data, OrderedDict) + or isinstance(data, list) + ) + if isinstance(data, dict) or isinstance(data, OrderedDict): + return data.keys() + elif isinstance(data, list): + return [key for (key, value) in data] + elif "_keys" in self.__dict__: + return self._keys + else: + return [] + + def popitem(self): + if not self._keys: + raise KeyError() + + key = self._keys.pop() + value = self[key] + del self[key] + return (key, value) + + def setdefault(self, key, failobj=None): + dict.setdefault(self, key, failobj) + if key not in self._keys: + self._keys.append(key) + + def update(self, data): + dict.update(self, data) + for key in self.keys(data): + if key not in self._keys: + self._keys.append(key) + + def values(self): + return map(self.get, self._keys) + + +###################################################################### +# Lazy Sequences +###################################################################### + + +@total_ordering +class AbstractLazySequence: + """ + An abstract base class for read-only sequences whose values are + computed as needed. Lazy sequences act like tuples -- they can be + indexed, sliced, and iterated over; but they may not be modified. + + The most common application of lazy sequences in NLTK is for + corpus view objects, which provide access to the contents of a + corpus without loading the entire corpus into memory, by loading + pieces of the corpus from disk as needed. + + The result of modifying a mutable element of a lazy sequence is + undefined. In particular, the modifications made to the element + may or may not persist, depending on whether and when the lazy + sequence caches that element's value or reconstructs it from + scratch. + + Subclasses are required to define two methods: ``__len__()`` + and ``iterate_from()``. + """ + + def __len__(self): + """ + Return the number of tokens in the corpus file underlying this + corpus view. + """ + raise NotImplementedError("should be implemented by subclass") + + def iterate_from(self, start): + """ + Return an iterator that generates the tokens in the corpus + file underlying this corpus view, starting at the token number + ``start``. If ``start>=len(self)``, then this iterator will + generate no tokens. + """ + raise NotImplementedError("should be implemented by subclass") + + def __getitem__(self, i): + """ + Return the *i* th token in the corpus file underlying this + corpus view. Negative indices and spans are both supported. + """ + if isinstance(i, slice): + start, stop = slice_bounds(self, i) + return LazySubsequence(self, start, stop) + else: + # Handle negative indices + if i < 0: + i += len(self) + if i < 0: + raise IndexError("index out of range") + # Use iterate_from to extract it. + try: + return next(self.iterate_from(i)) + except StopIteration as e: + raise IndexError("index out of range") from e + + def __iter__(self): + """Return an iterator that generates the tokens in the corpus + file underlying this corpus view.""" + return self.iterate_from(0) + + def count(self, value): + """Return the number of times this list contains ``value``.""" + return sum(1 for elt in self if elt == value) + + def index(self, value, start=None, stop=None): + """Return the index of the first occurrence of ``value`` in this + list that is greater than or equal to ``start`` and less than + ``stop``. Negative start and stop values are treated like negative + slice bounds -- i.e., they count from the end of the list.""" + start, stop = slice_bounds(self, slice(start, stop)) + for i, elt in enumerate(islice(self, start, stop)): + if elt == value: + return i + start + raise ValueError("index(x): x not in list") + + def __contains__(self, value): + """Return true if this list contains ``value``.""" + return bool(self.count(value)) + + def __add__(self, other): + """Return a list concatenating self with other.""" + return LazyConcatenation([self, other]) + + def __radd__(self, other): + """Return a list concatenating other with self.""" + return LazyConcatenation([other, self]) + + def __mul__(self, count): + """Return a list concatenating self with itself ``count`` times.""" + return LazyConcatenation([self] * count) + + def __rmul__(self, count): + """Return a list concatenating self with itself ``count`` times.""" + return LazyConcatenation([self] * count) + + _MAX_REPR_SIZE = 60 + + def __repr__(self): + """ + Return a string representation for this corpus view that is + similar to a list's representation; but if it would be more + than 60 characters long, it is truncated. + """ + pieces = [] + length = 5 + for elt in self: + pieces.append(repr(elt)) + length += len(pieces[-1]) + 2 + if length > self._MAX_REPR_SIZE and len(pieces) > 2: + return "[%s, ...]" % ", ".join(pieces[:-1]) + return "[%s]" % ", ".join(pieces) + + def __eq__(self, other): + return type(self) == type(other) and list(self) == list(other) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if type(other) != type(self): + raise_unorderable_types("<", self, other) + return list(self) < list(other) + + def __hash__(self): + """ + :raise ValueError: Corpus view objects are unhashable. + """ + raise ValueError("%s objects are unhashable" % self.__class__.__name__) + + +class LazySubsequence(AbstractLazySequence): + """ + A subsequence produced by slicing a lazy sequence. This slice + keeps a reference to its source sequence, and generates its values + by looking them up in the source sequence. + """ + + MIN_SIZE = 100 + """ + The minimum size for which lazy slices should be created. If + ``LazySubsequence()`` is called with a subsequence that is + shorter than ``MIN_SIZE``, then a tuple will be returned instead. + """ + + def __new__(cls, source, start, stop): + """ + Construct a new slice from a given underlying sequence. The + ``start`` and ``stop`` indices should be absolute indices -- + i.e., they should not be negative (for indexing from the back + of a list) or greater than the length of ``source``. + """ + # If the slice is small enough, just use a tuple. + if stop - start < cls.MIN_SIZE: + return list(islice(source.iterate_from(start), stop - start)) + else: + return object.__new__(cls) + + def __init__(self, source, start, stop): + self._source = source + self._start = start + self._stop = stop + + def __len__(self): + return self._stop - self._start + + def iterate_from(self, start): + return islice( + self._source.iterate_from(start + self._start), max(0, len(self) - start) + ) + + +class LazyConcatenation(AbstractLazySequence): + """ + A lazy sequence formed by concatenating a list of lists. This + underlying list of lists may itself be lazy. ``LazyConcatenation`` + maintains an index that it uses to keep track of the relationship + between offsets in the concatenated lists and offsets in the + sublists. + """ + + def __init__(self, list_of_lists): + self._list = list_of_lists + self._offsets = [0] + + def __len__(self): + if len(self._offsets) <= len(self._list): + for _ in self.iterate_from(self._offsets[-1]): + pass + return self._offsets[-1] + + def iterate_from(self, start_index): + if start_index < self._offsets[-1]: + sublist_index = bisect.bisect_right(self._offsets, start_index) - 1 + else: + sublist_index = len(self._offsets) - 1 + + index = self._offsets[sublist_index] + + # Construct an iterator over the sublists. + if isinstance(self._list, AbstractLazySequence): + sublist_iter = self._list.iterate_from(sublist_index) + else: + sublist_iter = islice(self._list, sublist_index, None) + + for sublist in sublist_iter: + if sublist_index == (len(self._offsets) - 1): + assert ( + index + len(sublist) >= self._offsets[-1] + ), "offsets not monotonic increasing!" + self._offsets.append(index + len(sublist)) + else: + assert self._offsets[sublist_index + 1] == index + len( + sublist + ), "inconsistent list value (num elts)" + + yield from sublist[max(0, start_index - index) :] + + index += len(sublist) + sublist_index += 1 + + +class LazyMap(AbstractLazySequence): + """ + A lazy sequence whose elements are formed by applying a given + function to each element in one or more underlying lists. The + function is applied lazily -- i.e., when you read a value from the + list, ``LazyMap`` will calculate that value by applying its + function to the underlying lists' value(s). ``LazyMap`` is + essentially a lazy version of the Python primitive function + ``map``. In particular, the following two expressions are + equivalent: + + >>> from nltk.collections import LazyMap + >>> function = str + >>> sequence = [1,2,3] + >>> map(function, sequence) # doctest: +SKIP + ['1', '2', '3'] + >>> list(LazyMap(function, sequence)) + ['1', '2', '3'] + + Like the Python ``map`` primitive, if the source lists do not have + equal size, then the value None will be supplied for the + 'missing' elements. + + Lazy maps can be useful for conserving memory, in cases where + individual values take up a lot of space. This is especially true + if the underlying list's values are constructed lazily, as is the + case with many corpus readers. + + A typical example of a use case for this class is performing + feature detection on the tokens in a corpus. Since featuresets + are encoded as dictionaries, which can take up a lot of memory, + using a ``LazyMap`` can significantly reduce memory usage when + training and running classifiers. + """ + + def __init__(self, function, *lists, **config): + """ + :param function: The function that should be applied to + elements of ``lists``. It should take as many arguments + as there are ``lists``. + :param lists: The underlying lists. + :param cache_size: Determines the size of the cache used + by this lazy map. (default=5) + """ + if not lists: + raise TypeError("LazyMap requires at least two args") + + self._lists = lists + self._func = function + self._cache_size = config.get("cache_size", 5) + self._cache = {} if self._cache_size > 0 else None + + # If you just take bool() of sum() here _all_lazy will be true just + # in case n >= 1 list is an AbstractLazySequence. Presumably this + # isn't what's intended. + self._all_lazy = sum( + isinstance(lst, AbstractLazySequence) for lst in lists + ) == len(lists) + + def iterate_from(self, index): + # Special case: one lazy sublist + if len(self._lists) == 1 and self._all_lazy: + for value in self._lists[0].iterate_from(index): + yield self._func(value) + return + + # Special case: one non-lazy sublist + elif len(self._lists) == 1: + while True: + try: + yield self._func(self._lists[0][index]) + except IndexError: + return + index += 1 + + # Special case: n lazy sublists + elif self._all_lazy: + iterators = [lst.iterate_from(index) for lst in self._lists] + while True: + elements = [] + for iterator in iterators: + try: + elements.append(next(iterator)) + # FIXME: What is this except really catching? StopIteration? + except StopIteration: + elements.append(None) + if elements == [None] * len(self._lists): + return + yield self._func(*elements) + index += 1 + + # general case + else: + while True: + try: + elements = [lst[index] for lst in self._lists] + except IndexError: + elements = [None] * len(self._lists) + for i, lst in enumerate(self._lists): + try: + elements[i] = lst[index] + except IndexError: + pass + if elements == [None] * len(self._lists): + return + yield self._func(*elements) + index += 1 + + def __getitem__(self, index): + if isinstance(index, slice): + sliced_lists = [lst[index] for lst in self._lists] + return LazyMap(self._func, *sliced_lists) + else: + # Handle negative indices + if index < 0: + index += len(self) + if index < 0: + raise IndexError("index out of range") + # Check the cache + if self._cache is not None and index in self._cache: + return self._cache[index] + # Calculate the value + try: + val = next(self.iterate_from(index)) + except StopIteration as e: + raise IndexError("index out of range") from e + # Update the cache + if self._cache is not None: + if len(self._cache) > self._cache_size: + self._cache.popitem() # discard random entry + self._cache[index] = val + # Return the value + return val + + def __len__(self): + return max(len(lst) for lst in self._lists) + + +class LazyZip(LazyMap): + """ + A lazy sequence whose elements are tuples, each containing the i-th + element from each of the argument sequences. The returned list is + truncated in length to the length of the shortest argument sequence. The + tuples are constructed lazily -- i.e., when you read a value from the + list, ``LazyZip`` will calculate that value by forming a tuple from + the i-th element of each of the argument sequences. + + ``LazyZip`` is essentially a lazy version of the Python primitive function + ``zip``. In particular, an evaluated LazyZip is equivalent to a zip: + + >>> from nltk.collections import LazyZip + >>> sequence1, sequence2 = [1, 2, 3], ['a', 'b', 'c'] + >>> zip(sequence1, sequence2) # doctest: +SKIP + [(1, 'a'), (2, 'b'), (3, 'c')] + >>> list(LazyZip(sequence1, sequence2)) + [(1, 'a'), (2, 'b'), (3, 'c')] + >>> sequences = [sequence1, sequence2, [6,7,8,9]] + >>> list(zip(*sequences)) == list(LazyZip(*sequences)) + True + + Lazy zips can be useful for conserving memory in cases where the argument + sequences are particularly long. + + A typical example of a use case for this class is combining long sequences + of gold standard and predicted values in a classification or tagging task + in order to calculate accuracy. By constructing tuples lazily and + avoiding the creation of an additional long sequence, memory usage can be + significantly reduced. + """ + + def __init__(self, *lists): + """ + :param lists: the underlying lists + :type lists: list(list) + """ + LazyMap.__init__(self, lambda *elts: elts, *lists) + + def iterate_from(self, index): + iterator = LazyMap.iterate_from(self, index) + while index < len(self): + yield next(iterator) + index += 1 + return + + def __len__(self): + return min(len(lst) for lst in self._lists) + + +class LazyEnumerate(LazyZip): + """ + A lazy sequence whose elements are tuples, each containing a count (from + zero) and a value yielded by underlying sequence. ``LazyEnumerate`` is + useful for obtaining an indexed list. The tuples are constructed lazily + -- i.e., when you read a value from the list, ``LazyEnumerate`` will + calculate that value by forming a tuple from the count of the i-th + element and the i-th element of the underlying sequence. + + ``LazyEnumerate`` is essentially a lazy version of the Python primitive + function ``enumerate``. In particular, the following two expressions are + equivalent: + + >>> from nltk.collections import LazyEnumerate + >>> sequence = ['first', 'second', 'third'] + >>> list(enumerate(sequence)) + [(0, 'first'), (1, 'second'), (2, 'third')] + >>> list(LazyEnumerate(sequence)) + [(0, 'first'), (1, 'second'), (2, 'third')] + + Lazy enumerations can be useful for conserving memory in cases where the + argument sequences are particularly long. + + A typical example of a use case for this class is obtaining an indexed + list for a long sequence of values. By constructing tuples lazily and + avoiding the creation of an additional long sequence, memory usage can be + significantly reduced. + """ + + def __init__(self, lst): + """ + :param lst: the underlying list + :type lst: list + """ + LazyZip.__init__(self, range(len(lst)), lst) + + +class LazyIteratorList(AbstractLazySequence): + """ + Wraps an iterator, loading its elements on demand + and making them subscriptable. + __repr__ displays only the first few elements. + """ + + def __init__(self, it, known_len=None): + self._it = it + self._len = known_len + self._cache = [] + + def __len__(self): + if self._len: + return self._len + for _ in self.iterate_from(len(self._cache)): + pass + self._len = len(self._cache) + return self._len + + def iterate_from(self, start): + """Create a new iterator over this list starting at the given offset.""" + while len(self._cache) < start: + v = next(self._it) + self._cache.append(v) + i = start + while i < len(self._cache): + yield self._cache[i] + i += 1 + try: + while True: + v = next(self._it) + self._cache.append(v) + yield v + except StopIteration: + pass + + def __add__(self, other): + """Return a list concatenating self with other.""" + return type(self)(chain(self, other)) + + def __radd__(self, other): + """Return a list concatenating other with self.""" + return type(self)(chain(other, self)) + + +###################################################################### +# Trie Implementation +###################################################################### +class Trie(dict): + """A Trie implementation for strings""" + + LEAF = True + + def __init__(self, strings=None): + """Builds a Trie object, which is built around a ``dict`` + + If ``strings`` is provided, it will add the ``strings``, which + consist of a ``list`` of ``strings``, to the Trie. + Otherwise, it'll construct an empty Trie. + + :param strings: List of strings to insert into the trie + (Default is ``None``) + :type strings: list(str) + + """ + super().__init__() + if strings: + for string in strings: + self.insert(string) + + def insert(self, string): + """Inserts ``string`` into the Trie + + :param string: String to insert into the trie + :type string: str + + :Example: + + >>> from nltk.collections import Trie + >>> trie = Trie(["abc", "def"]) + >>> expected = {'a': {'b': {'c': {True: None}}}, \ + 'd': {'e': {'f': {True: None}}}} + >>> trie == expected + True + + """ + if len(string): + self[string[0]].insert(string[1:]) + else: + # mark the string is complete + self[Trie.LEAF] = None + + def __missing__(self, key): + self[key] = Trie() + return self[key] diff --git a/nltk/collocations.py b/nltk/collocations.py new file mode 100644 index 0000000..2138975 --- /dev/null +++ b/nltk/collocations.py @@ -0,0 +1,421 @@ +# Natural Language Toolkit: Collocations and Association Measures +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Joel Nothman +# URL: +# For license information, see LICENSE.TXT +# +""" +Tools to identify collocations --- words that often appear consecutively +--- within corpora. They may also be used to find other associations between +word occurrences. +See Manning and Schutze ch. 5 at https://nlp.stanford.edu/fsnlp/promo/colloc.pdf +and the Text::NSP Perl package at http://ngram.sourceforge.net + +Finding collocations requires first calculating the frequencies of words and +their appearance in the context of other words. Often the collection of words +will then requiring filtering to only retain useful content terms. Each ngram +of words may then be scored according to some association measure, in order +to determine the relative likelihood of each ngram being a collocation. + +The ``BigramCollocationFinder`` and ``TrigramCollocationFinder`` classes provide +these functionalities, dependent on being provided a function which scores a +ngram given appropriate frequency counts. A number of standard association +measures are provided in bigram_measures and trigram_measures. +""" + +# Possible TODOs: +# - consider the distinction between f(x,_) and f(x) and whether our +# approximation is good enough for fragmented data, and mention it +# - add a n-gram collocation finder with measures which only utilise n-gram +# and unigram counts (raw_freq, pmi, student_t) + +import itertools as _itertools + +# these two unused imports are referenced in collocations.doctest +from nltk.metrics import ( + BigramAssocMeasures, + ContingencyMeasures, + QuadgramAssocMeasures, + TrigramAssocMeasures, +) +from nltk.probability import FreqDist +from nltk.util import ngrams + + +class AbstractCollocationFinder: + """ + An abstract base class for collocation finders whose purpose is to + collect collocation candidate frequencies, filter and rank them. + + As a minimum, collocation finders require the frequencies of each + word in a corpus, and the joint frequency of word tuples. This data + should be provided through nltk.probability.FreqDist objects or an + identical interface. + """ + + def __init__(self, word_fd, ngram_fd): + self.word_fd = word_fd + self.N = word_fd.N() + self.ngram_fd = ngram_fd + + @classmethod + def _build_new_documents( + cls, documents, window_size, pad_left=False, pad_right=False, pad_symbol=None + ): + """ + Pad the document with the place holder according to the window_size + """ + padding = (pad_symbol,) * (window_size - 1) + if pad_right: + return _itertools.chain.from_iterable( + _itertools.chain(doc, padding) for doc in documents + ) + if pad_left: + return _itertools.chain.from_iterable( + _itertools.chain(padding, doc) for doc in documents + ) + + @classmethod + def from_documents(cls, documents): + """Constructs a collocation finder given a collection of documents, + each of which is a list (or iterable) of tokens. + """ + # return cls.from_words(_itertools.chain(*documents)) + return cls.from_words( + cls._build_new_documents(documents, cls.default_ws, pad_right=True) + ) + + @staticmethod + def _ngram_freqdist(words, n): + return FreqDist(tuple(words[i : i + n]) for i in range(len(words) - 1)) + + def _apply_filter(self, fn=lambda ngram, freq: False): + """Generic filter removes ngrams from the frequency distribution + if the function returns True when passed an ngram tuple. + """ + tmp_ngram = FreqDist() + for ngram, freq in self.ngram_fd.items(): + if not fn(ngram, freq): + tmp_ngram[ngram] = freq + self.ngram_fd = tmp_ngram + + def apply_freq_filter(self, min_freq): + """Removes candidate ngrams which have frequency less than min_freq.""" + self._apply_filter(lambda ng, freq: freq < min_freq) + + def apply_ngram_filter(self, fn): + """Removes candidate ngrams (w1, w2, ...) where fn(w1, w2, ...) + evaluates to True. + """ + self._apply_filter(lambda ng, f: fn(*ng)) + + def apply_word_filter(self, fn): + """Removes candidate ngrams (w1, w2, ...) where any of (fn(w1), fn(w2), + ...) evaluates to True. + """ + self._apply_filter(lambda ng, f: any(fn(w) for w in ng)) + + def _score_ngrams(self, score_fn): + """Generates of (ngram, score) pairs as determined by the scoring + function provided. + """ + for tup in self.ngram_fd: + score = self.score_ngram(score_fn, *tup) + if score is not None: + yield tup, score + + def score_ngrams(self, score_fn): + """Returns a sequence of (ngram, score) pairs ordered from highest to + lowest score, as determined by the scoring function provided. + """ + return sorted(self._score_ngrams(score_fn), key=lambda t: (-t[1], t[0])) + + def nbest(self, score_fn, n): + """Returns the top n ngrams when scored by the given function.""" + return [p for p, s in self.score_ngrams(score_fn)[:n]] + + def above_score(self, score_fn, min_score): + """Returns a sequence of ngrams, ordered by decreasing score, whose + scores each exceed the given minimum score. + """ + for ngram, score in self.score_ngrams(score_fn): + if score > min_score: + yield ngram + else: + break + + +class BigramCollocationFinder(AbstractCollocationFinder): + """A tool for the finding and ranking of bigram collocations or other + association measures. It is often useful to use from_words() rather than + constructing an instance directly. + """ + + default_ws = 2 + + def __init__(self, word_fd, bigram_fd, window_size=2): + """Construct a BigramCollocationFinder, given FreqDists for + appearances of words and (possibly non-contiguous) bigrams. + """ + AbstractCollocationFinder.__init__(self, word_fd, bigram_fd) + self.window_size = window_size + + @classmethod + def from_words(cls, words, window_size=2): + """Construct a BigramCollocationFinder for all bigrams in the given + sequence. When window_size > 2, count non-contiguous bigrams, in the + style of Church and Hanks's (1990) association ratio. + """ + wfd = FreqDist() + bfd = FreqDist() + + if window_size < 2: + raise ValueError("Specify window_size at least 2") + + for window in ngrams(words, window_size, pad_right=True): + w1 = window[0] + if w1 is None: + continue + wfd[w1] += 1 + for w2 in window[1:]: + if w2 is not None: + bfd[(w1, w2)] += 1 + return cls(wfd, bfd, window_size=window_size) + + def score_ngram(self, score_fn, w1, w2): + """Returns the score for a given bigram using the given scoring + function. Following Church and Hanks (1990), counts are scaled by + a factor of 1/(window_size - 1). + """ + n_all = self.N + n_ii = self.ngram_fd[(w1, w2)] / (self.window_size - 1.0) + if not n_ii: + return + n_ix = self.word_fd[w1] + n_xi = self.word_fd[w2] + return score_fn(n_ii, (n_ix, n_xi), n_all) + + +class TrigramCollocationFinder(AbstractCollocationFinder): + """A tool for the finding and ranking of trigram collocations or other + association measures. It is often useful to use from_words() rather than + constructing an instance directly. + """ + + default_ws = 3 + + def __init__(self, word_fd, bigram_fd, wildcard_fd, trigram_fd): + """Construct a TrigramCollocationFinder, given FreqDists for + appearances of words, bigrams, two words with any word between them, + and trigrams. + """ + AbstractCollocationFinder.__init__(self, word_fd, trigram_fd) + self.wildcard_fd = wildcard_fd + self.bigram_fd = bigram_fd + + @classmethod + def from_words(cls, words, window_size=3): + """Construct a TrigramCollocationFinder for all trigrams in the given + sequence. + """ + if window_size < 3: + raise ValueError("Specify window_size at least 3") + + wfd = FreqDist() + wildfd = FreqDist() + bfd = FreqDist() + tfd = FreqDist() + for window in ngrams(words, window_size, pad_right=True): + w1 = window[0] + if w1 is None: + continue + for w2, w3 in _itertools.combinations(window[1:], 2): + wfd[w1] += 1 + if w2 is None: + continue + bfd[(w1, w2)] += 1 + if w3 is None: + continue + wildfd[(w1, w3)] += 1 + tfd[(w1, w2, w3)] += 1 + return cls(wfd, bfd, wildfd, tfd) + + def bigram_finder(self): + """Constructs a bigram collocation finder with the bigram and unigram + data from this finder. Note that this does not include any filtering + applied to this finder. + """ + return BigramCollocationFinder(self.word_fd, self.bigram_fd) + + def score_ngram(self, score_fn, w1, w2, w3): + """Returns the score for a given trigram using the given scoring + function. + """ + n_all = self.N + n_iii = self.ngram_fd[(w1, w2, w3)] + if not n_iii: + return + n_iix = self.bigram_fd[(w1, w2)] + n_ixi = self.wildcard_fd[(w1, w3)] + n_xii = self.bigram_fd[(w2, w3)] + n_ixx = self.word_fd[w1] + n_xix = self.word_fd[w2] + n_xxi = self.word_fd[w3] + return score_fn(n_iii, (n_iix, n_ixi, n_xii), (n_ixx, n_xix, n_xxi), n_all) + + +class QuadgramCollocationFinder(AbstractCollocationFinder): + """A tool for the finding and ranking of quadgram collocations or other association measures. + It is often useful to use from_words() rather than constructing an instance directly. + """ + + default_ws = 4 + + def __init__(self, word_fd, quadgram_fd, ii, iii, ixi, ixxi, iixi, ixii): + """Construct a QuadgramCollocationFinder, given FreqDists for appearances of words, + bigrams, trigrams, two words with one word and two words between them, three words + with a word between them in both variations. + """ + AbstractCollocationFinder.__init__(self, word_fd, quadgram_fd) + self.iii = iii + self.ii = ii + self.ixi = ixi + self.ixxi = ixxi + self.iixi = iixi + self.ixii = ixii + + @classmethod + def from_words(cls, words, window_size=4): + if window_size < 4: + raise ValueError("Specify window_size at least 4") + ixxx = FreqDist() + iiii = FreqDist() + ii = FreqDist() + iii = FreqDist() + ixi = FreqDist() + ixxi = FreqDist() + iixi = FreqDist() + ixii = FreqDist() + + for window in ngrams(words, window_size, pad_right=True): + w1 = window[0] + if w1 is None: + continue + for w2, w3, w4 in _itertools.combinations(window[1:], 3): + ixxx[w1] += 1 + if w2 is None: + continue + ii[(w1, w2)] += 1 + if w3 is None: + continue + iii[(w1, w2, w3)] += 1 + ixi[(w1, w3)] += 1 + if w4 is None: + continue + iiii[(w1, w2, w3, w4)] += 1 + ixxi[(w1, w4)] += 1 + ixii[(w1, w3, w4)] += 1 + iixi[(w1, w2, w4)] += 1 + + return cls(ixxx, iiii, ii, iii, ixi, ixxi, iixi, ixii) + + def score_ngram(self, score_fn, w1, w2, w3, w4): + n_all = self.N + n_iiii = self.ngram_fd[(w1, w2, w3, w4)] + if not n_iiii: + return + n_iiix = self.iii[(w1, w2, w3)] + n_xiii = self.iii[(w2, w3, w4)] + n_iixi = self.iixi[(w1, w2, w4)] + n_ixii = self.ixii[(w1, w3, w4)] + + n_iixx = self.ii[(w1, w2)] + n_xxii = self.ii[(w3, w4)] + n_xiix = self.ii[(w2, w3)] + n_ixix = self.ixi[(w1, w3)] + n_ixxi = self.ixxi[(w1, w4)] + n_xixi = self.ixi[(w2, w4)] + + n_ixxx = self.word_fd[w1] + n_xixx = self.word_fd[w2] + n_xxix = self.word_fd[w3] + n_xxxi = self.word_fd[w4] + return score_fn( + n_iiii, + (n_iiix, n_iixi, n_ixii, n_xiii), + (n_iixx, n_ixix, n_ixxi, n_xixi, n_xxii, n_xiix), + (n_ixxx, n_xixx, n_xxix, n_xxxi), + n_all, + ) + + +def demo(scorer=None, compare_scorer=None): + """Finds bigram collocations in the files of the WebText corpus.""" + from nltk.metrics import ( + BigramAssocMeasures, + ranks_from_scores, + spearman_correlation, + ) + + if scorer is None: + scorer = BigramAssocMeasures.likelihood_ratio + if compare_scorer is None: + compare_scorer = BigramAssocMeasures.raw_freq + + from nltk.corpus import stopwords, webtext + + ignored_words = stopwords.words("english") + word_filter = lambda w: len(w) < 3 or w.lower() in ignored_words + + for file in webtext.fileids(): + words = [word.lower() for word in webtext.words(file)] + + cf = BigramCollocationFinder.from_words(words) + cf.apply_freq_filter(3) + cf.apply_word_filter(word_filter) + + corr = spearman_correlation( + ranks_from_scores(cf.score_ngrams(scorer)), + ranks_from_scores(cf.score_ngrams(compare_scorer)), + ) + print(file) + print("\t", [" ".join(tup) for tup in cf.nbest(scorer, 15)]) + print(f"\t Correlation to {compare_scorer.__name__}: {corr:0.4f}") + + +# Slows down loading too much +# bigram_measures = BigramAssocMeasures() +# trigram_measures = TrigramAssocMeasures() + +# Command-line interface for demonstrating bigram collocations. +# +# Usage: python -m nltk.collocations [scorer] [compare_scorer] +# +# Demonstrates bigram collocations on the WebText corpus. +# Defaults to likelihood_ratio and raw_freq if not specified. +# +# Available scorers: +# chi_sq, dice, fisher, jaccard, likelihood_ratio, mi_like, +# phi_sq, pmi, poisson_stirling, raw_freq, student_t +if __name__ == "__main__": + import sys + + from nltk.metrics import BigramAssocMeasures + + try: + scorer = getattr(BigramAssocMeasures, sys.argv[1], None) + except IndexError: + scorer = None + try: + compare_scorer = getattr(BigramAssocMeasures, sys.argv[2], None) + except IndexError: + compare_scorer = None + + demo(scorer, compare_scorer) + +__all__ = [ + "BigramCollocationFinder", + "TrigramCollocationFinder", + "QuadgramCollocationFinder", +] diff --git a/nltk/compat.py b/nltk/compat.py new file mode 100755 index 0000000..6b9045b --- /dev/null +++ b/nltk/compat.py @@ -0,0 +1,38 @@ +# Natural Language Toolkit: Compatibility +# +# Copyright (C) 2001-2026 NLTK Project +# +# URL: +# For license information, see LICENSE.TXT + +import os +from functools import wraps + +# ======= Compatibility for datasets that care about Python versions ======== + +# The following datasets have a /PY3 subdirectory containing +# a full copy of the data which has been re-encoded or repickled. +DATA_UPDATES = [] + +_PY3_DATA_UPDATES = [os.path.join(*path_list) for path_list in DATA_UPDATES] + + +def add_py3_data(path): + for item in _PY3_DATA_UPDATES: + if item in str(path) and "/PY3" not in str(path): + pos = path.index(item) + len(item) + if path[pos : pos + 4] == ".zip": + pos += 4 + path = path[:pos] + "/PY3" + path[pos:] + break + return path + + +# for use in adding /PY3 to the second (filename) argument +# of the file pointers in data.py +def py3_data(init_func): + def _decorator(*args, **kwargs): + args = (args[0], add_py3_data(args[1])) + args[2:] + return init_func(*args, **kwargs) + + return wraps(init_func)(_decorator) diff --git a/nltk/corpus/__init__.py b/nltk/corpus/__init__.py new file mode 100644 index 0000000..f77214c --- /dev/null +++ b/nltk/corpus/__init__.py @@ -0,0 +1,551 @@ +# Natural Language Toolkit: Corpus Readers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +# TODO this docstring isn't up-to-date! +""" +NLTK corpus readers. The modules in this package provide functions +that can be used to read corpus files in a variety of formats. These +functions can be used to read both the corpus files that are +distributed in the NLTK corpus package, and corpus files that are part +of external corpora. + +Available Corpora +================= + +Please see https://www.nltk.org/nltk_data/ for a complete list. +Install corpora using nltk.download(). + +Corpus Reader Functions +======================= +Each corpus module defines one or more "corpus reader functions", +which can be used to read documents from that corpus. These functions +take an argument, ``item``, which is used to indicate which document +should be read from the corpus: + +- If ``item`` is one of the unique identifiers listed in the corpus + module's ``items`` variable, then the corresponding document will + be loaded from the NLTK corpus package. +- If ``item`` is a filename, then that file will be read. + +Additionally, corpus reader functions can be given lists of item +names; in which case, they will return a concatenation of the +corresponding documents. + +Corpus reader functions are named based on the type of information +they return. Some common examples, and their return types, are: + +- words(): list of str +- sents(): list of (list of str) +- paras(): list of (list of (list of str)) +- tagged_words(): list of (str,str) tuple +- tagged_sents(): list of (list of (str,str)) +- tagged_paras(): list of (list of (list of (str,str))) +- chunked_sents(): list of (Tree w/ (str,str) leaves) +- parsed_sents(): list of (Tree with str leaves) +- parsed_paras(): list of (list of (Tree with str leaves)) +- xml(): A single xml ElementTree +- raw(): unprocessed corpus contents + +For example, to read a list of the words in the Brown Corpus, use +``nltk.corpus.brown.words()``: + + >>> from nltk.corpus import brown + >>> print(", ".join(brown.words())) # doctest: +ELLIPSIS + The, Fulton, County, Grand, Jury, said, ... + +""" + +import re + +from nltk.corpus.reader import * +from nltk.corpus.util import LazyCorpusLoader +from nltk.tokenize import RegexpTokenizer + +abc: PlaintextCorpusReader = LazyCorpusLoader( + "abc", + PlaintextCorpusReader, + r"(?!\.).*\.txt", + encoding=[("science", "latin_1"), ("rural", "utf8")], +) +alpino: AlpinoCorpusReader = LazyCorpusLoader( + "alpino", AlpinoCorpusReader, tagset="alpino" +) +bcp47: BCP47CorpusReader = LazyCorpusLoader( + "bcp47", BCP47CorpusReader, r"(cldr|iana)/*" +) +brown: CategorizedTaggedCorpusReader = LazyCorpusLoader( + "brown", + CategorizedTaggedCorpusReader, + r"c[a-z]\d\d", + cat_file="cats.txt", + tagset="brown", + encoding="ascii", +) +cess_cat: BracketParseCorpusReader = LazyCorpusLoader( + "cess_cat", + BracketParseCorpusReader, + r"(?!\.).*\.tbf", + tagset="unknown", + encoding="ISO-8859-15", +) +cess_esp: BracketParseCorpusReader = LazyCorpusLoader( + "cess_esp", + BracketParseCorpusReader, + r"(?!\.).*\.tbf", + tagset="unknown", + encoding="ISO-8859-15", +) +cmudict: CMUDictCorpusReader = LazyCorpusLoader( + "cmudict", CMUDictCorpusReader, ["cmudict"] +) +comtrans: AlignedCorpusReader = LazyCorpusLoader( + "comtrans", AlignedCorpusReader, r"(?!\.).*\.txt" +) +comparative_sentences: ComparativeSentencesCorpusReader = LazyCorpusLoader( + "comparative_sentences", + ComparativeSentencesCorpusReader, + r"labeledSentences\.txt", + encoding="latin-1", +) +conll2000: ConllChunkCorpusReader = LazyCorpusLoader( + "conll2000", + ConllChunkCorpusReader, + ["train.txt", "test.txt"], + ("NP", "VP", "PP"), + tagset="wsj", + encoding="ascii", +) +conll2002: ConllChunkCorpusReader = LazyCorpusLoader( + "conll2002", + ConllChunkCorpusReader, + r".*\.(test|train).*", + ("LOC", "PER", "ORG", "MISC"), + encoding="utf-8", +) +conll2007: DependencyCorpusReader = LazyCorpusLoader( + "conll2007", + DependencyCorpusReader, + r".*\.(test|train).*", + encoding=[("eus", "ISO-8859-2"), ("esp", "utf8")], +) +crubadan: CrubadanCorpusReader = LazyCorpusLoader( + "crubadan", CrubadanCorpusReader, r".*\.txt" +) +dependency_treebank: DependencyCorpusReader = LazyCorpusLoader( + "dependency_treebank", DependencyCorpusReader, r".*\.dp", encoding="ascii" +) +extended_omw: CorpusReader = LazyCorpusLoader( + "extended_omw", CorpusReader, r".*/wn-[a-z\-]*\.tab", encoding="utf8" +) +floresta: BracketParseCorpusReader = LazyCorpusLoader( + "floresta", + BracketParseCorpusReader, + r"(?!\.).*\.ptb", + "#", + tagset="unknown", + encoding="ISO-8859-15", +) +framenet15: FramenetCorpusReader = LazyCorpusLoader( + "framenet_v15", + FramenetCorpusReader, + [ + "frRelation.xml", + "frameIndex.xml", + "fulltextIndex.xml", + "luIndex.xml", + "semTypes.xml", + ], +) +framenet: FramenetCorpusReader = LazyCorpusLoader( + "framenet_v17", + FramenetCorpusReader, + [ + "frRelation.xml", + "frameIndex.xml", + "fulltextIndex.xml", + "luIndex.xml", + "semTypes.xml", + ], +) +gazetteers: WordListCorpusReader = LazyCorpusLoader( + "gazetteers", WordListCorpusReader, r"(?!LICENSE|\.).*\.txt", encoding="ISO-8859-2" +) +genesis: PlaintextCorpusReader = LazyCorpusLoader( + "genesis", + PlaintextCorpusReader, + r"(?!\.).*\.txt", + encoding=[ + ("finnish|french|german", "latin_1"), + ("swedish", "cp865"), + (".*", "utf_8"), + ], +) +gutenberg: PlaintextCorpusReader = LazyCorpusLoader( + "gutenberg", PlaintextCorpusReader, r"(?!\.).*\.txt", encoding="latin1" +) +ieer: IEERCorpusReader = LazyCorpusLoader("ieer", IEERCorpusReader, r"(?!README|\.).*") +inaugural: PlaintextCorpusReader = LazyCorpusLoader( + "inaugural", PlaintextCorpusReader, r"(?!\.).*\.txt", encoding="latin1" +) +# [XX] This should probably just use TaggedCorpusReader: +indian: IndianCorpusReader = LazyCorpusLoader( + "indian", IndianCorpusReader, r"(?!\.).*\.pos", tagset="unknown", encoding="utf8" +) + +jeita: ChasenCorpusReader = LazyCorpusLoader( + "jeita", ChasenCorpusReader, r".*\.chasen", encoding="utf-8" +) +knbc: KNBCorpusReader = LazyCorpusLoader( + "knbc/corpus1", KNBCorpusReader, r".*/KN.*", encoding="euc-jp" +) +lin_thesaurus: LinThesaurusCorpusReader = LazyCorpusLoader( + "lin_thesaurus", LinThesaurusCorpusReader, r".*\.lsp" +) +mac_morpho: MacMorphoCorpusReader = LazyCorpusLoader( + "mac_morpho", + MacMorphoCorpusReader, + r"(?!\.).*\.txt", + tagset="unknown", + encoding="latin-1", +) +machado: PortugueseCategorizedPlaintextCorpusReader = LazyCorpusLoader( + "machado", + PortugueseCategorizedPlaintextCorpusReader, + r"(?!\.).*\.txt", + cat_pattern=r"([a-z]*)/.*", + encoding="latin-1", +) +masc_tagged: CategorizedTaggedCorpusReader = LazyCorpusLoader( + "masc_tagged", + CategorizedTaggedCorpusReader, + r"(spoken|written)/.*\.txt", + cat_file="categories.txt", + tagset="wsj", + encoding="utf-8", + sep="_", +) +movie_reviews: CategorizedPlaintextCorpusReader = LazyCorpusLoader( + "movie_reviews", + CategorizedPlaintextCorpusReader, + r"(?!\.).*\.txt", + cat_pattern=r"(neg|pos)/.*", + encoding="ascii", +) +multext_east: MTECorpusReader = LazyCorpusLoader( + "mte_teip5", MTECorpusReader, r"(oana).*\.xml", encoding="utf-8" +) +names: WordListCorpusReader = LazyCorpusLoader( + "names", WordListCorpusReader, r"(?!\.).*\.txt", encoding="ascii" +) +nps_chat: NPSChatCorpusReader = LazyCorpusLoader( + "nps_chat", NPSChatCorpusReader, r"(?!README|\.).*\.xml", tagset="wsj" +) +opinion_lexicon: OpinionLexiconCorpusReader = LazyCorpusLoader( + "opinion_lexicon", + OpinionLexiconCorpusReader, + r"(\w+)\-words\.txt", + encoding="ISO-8859-2", +) +ppattach: PPAttachmentCorpusReader = LazyCorpusLoader( + "ppattach", PPAttachmentCorpusReader, ["training", "test", "devset"] +) +product_reviews_1: ReviewsCorpusReader = LazyCorpusLoader( + "product_reviews_1", ReviewsCorpusReader, r"^(?!Readme).*\.txt", encoding="utf8" +) +product_reviews_2: ReviewsCorpusReader = LazyCorpusLoader( + "product_reviews_2", ReviewsCorpusReader, r"^(?!Readme).*\.txt", encoding="utf8" +) +pros_cons: ProsConsCorpusReader = LazyCorpusLoader( + "pros_cons", + ProsConsCorpusReader, + r"Integrated(Cons|Pros)\.txt", + cat_pattern=r"Integrated(Cons|Pros)\.txt", + encoding="ISO-8859-2", +) +ptb: CategorizedBracketParseCorpusReader = ( + LazyCorpusLoader( # Penn Treebank v3: WSJ and Brown portions + "ptb", + CategorizedBracketParseCorpusReader, + r"(WSJ/\d\d/WSJ_\d\d|BROWN/C[A-Z]/C[A-Z])\d\d.MRG", + cat_file="allcats.txt", + tagset="wsj", + ) +) +qc: StringCategoryCorpusReader = LazyCorpusLoader( + "qc", StringCategoryCorpusReader, ["train.txt", "test.txt"], encoding="ISO-8859-2" +) +reuters: CategorizedPlaintextCorpusReader = LazyCorpusLoader( + "reuters", + CategorizedPlaintextCorpusReader, + "(training|test).*", + cat_file="cats.txt", + encoding="ISO-8859-2", +) +rte: RTECorpusReader = LazyCorpusLoader("rte", RTECorpusReader, r"(?!\.).*\.xml") +senseval: SensevalCorpusReader = LazyCorpusLoader( + "senseval", SensevalCorpusReader, r"(?!\.).*\.pos" +) +sentence_polarity: CategorizedSentencesCorpusReader = LazyCorpusLoader( + "sentence_polarity", + CategorizedSentencesCorpusReader, + r"rt-polarity\.(neg|pos)", + cat_pattern=r"rt-polarity\.(neg|pos)", + encoding="utf-8", +) +sentiwordnet: SentiWordNetCorpusReader = LazyCorpusLoader( + "sentiwordnet", SentiWordNetCorpusReader, "SentiWordNet_3.0.0.txt", encoding="utf-8" +) +shakespeare: XMLCorpusReader = LazyCorpusLoader( + "shakespeare", XMLCorpusReader, r"(?!\.).*\.xml" +) +sinica_treebank: SinicaTreebankCorpusReader = LazyCorpusLoader( + "sinica_treebank", + SinicaTreebankCorpusReader, + ["parsed"], + tagset="unknown", + encoding="utf-8", +) +state_union: PlaintextCorpusReader = LazyCorpusLoader( + "state_union", PlaintextCorpusReader, r"(?!\.).*\.txt", encoding="ISO-8859-2" +) +stopwords: WordListCorpusReader = LazyCorpusLoader( + "stopwords", WordListCorpusReader, r"(?!README|\.).*", encoding="utf8" +) +subjectivity: CategorizedSentencesCorpusReader = LazyCorpusLoader( + "subjectivity", + CategorizedSentencesCorpusReader, + r"(quote.tok.gt9|plot.tok.gt9)\.5000", + cat_map={"quote.tok.gt9.5000": ["subj"], "plot.tok.gt9.5000": ["obj"]}, + encoding="latin-1", +) +swadesh: SwadeshCorpusReader = LazyCorpusLoader( + "swadesh", SwadeshCorpusReader, r"(?!README|\.).*", encoding="utf8" +) +swadesh110: PanlexSwadeshCorpusReader = LazyCorpusLoader( + "panlex_swadesh", PanlexSwadeshCorpusReader, r"swadesh110/.*\.txt", encoding="utf8" +) +swadesh207: PanlexSwadeshCorpusReader = LazyCorpusLoader( + "panlex_swadesh", PanlexSwadeshCorpusReader, r"swadesh207/.*\.txt", encoding="utf8" +) +switchboard: SwitchboardCorpusReader = LazyCorpusLoader( + "switchboard", SwitchboardCorpusReader, tagset="wsj" +) +timit: TimitCorpusReader = LazyCorpusLoader("timit", TimitCorpusReader) +timit_tagged: TimitTaggedCorpusReader = LazyCorpusLoader( + "timit", TimitTaggedCorpusReader, r".+\.tags", tagset="wsj", encoding="ascii" +) +toolbox: ToolboxCorpusReader = LazyCorpusLoader( + "toolbox", ToolboxCorpusReader, r"(?!.*(README|\.)).*\.(dic|txt)" +) +treebank: BracketParseCorpusReader = LazyCorpusLoader( + "treebank/combined", + BracketParseCorpusReader, + r"wsj_.*\.mrg", + tagset="wsj", + encoding="ascii", +) +treebank_chunk: ChunkedCorpusReader = LazyCorpusLoader( + "treebank/tagged", + ChunkedCorpusReader, + r"wsj_.*\.pos", + sent_tokenizer=RegexpTokenizer(r"(?<=/\.)\s*(?![^\[]*\])", gaps=True), + para_block_reader=tagged_treebank_para_block_reader, + tagset="wsj", + encoding="ascii", +) +treebank_raw: PlaintextCorpusReader = LazyCorpusLoader( + "treebank/raw", PlaintextCorpusReader, r"wsj_.*", encoding="ISO-8859-2" +) +twitter_samples: TwitterCorpusReader = LazyCorpusLoader( + "twitter_samples", TwitterCorpusReader, r".*\.json" +) +udhr: UdhrCorpusReader = LazyCorpusLoader("udhr", UdhrCorpusReader) +udhr2: PlaintextCorpusReader = LazyCorpusLoader( + "udhr2", PlaintextCorpusReader, r".*\.txt", encoding="utf8" +) +universal_treebanks: ConllCorpusReader = LazyCorpusLoader( + "universal_treebanks_v20", + ConllCorpusReader, + r".*\.conll", + columntypes=( + "ignore", + "words", + "ignore", + "ignore", + "pos", + "ignore", + "ignore", + "ignore", + "ignore", + "ignore", + ), +) +verbnet: VerbnetCorpusReader = LazyCorpusLoader( + "verbnet", VerbnetCorpusReader, r"(?!\.).*\.xml" +) +webtext: PlaintextCorpusReader = LazyCorpusLoader( + "webtext", PlaintextCorpusReader, r"(?!README|\.).*\.txt", encoding="ISO-8859-2" +) +wordnet: WordNetCorpusReader = LazyCorpusLoader( + "wordnet", + WordNetCorpusReader, + LazyCorpusLoader("omw-2.0", CorpusReader, r".*/wn-data-.*\.tab", encoding="utf8"), +) +## Use the following template to add a custom Wordnet package. +## Just uncomment, and replace the identifier (my_wordnet) in two places: +## +# my_wordnet: WordNetCorpusReader = LazyCorpusLoader( +# "my_wordnet", +# WordNetCorpusReader, +# LazyCorpusLoader("omw-2.0", CorpusReader, r".*/wn-data-.*\.tab", encoding="utf8"), +# ) +wordnet31: WordNetCorpusReader = LazyCorpusLoader( + "wordnet31", + WordNetCorpusReader, + LazyCorpusLoader("omw-2.0", CorpusReader, r".*/wn-data-.*\.tab", encoding="utf8"), +) +wordnet2021: WordNetCorpusReader = LazyCorpusLoader( + # Obsolete, use english_wordnet instead. + "wordnet2021", + WordNetCorpusReader, + LazyCorpusLoader("omw-2.0", CorpusReader, r".*/wn-data-.*\.tab", encoding="utf8"), +) +wordnet2022: WordNetCorpusReader = LazyCorpusLoader( + # Obsolete, use english_wordnet instead. + "wordnet2022", + WordNetCorpusReader, + LazyCorpusLoader("omw-2.0", CorpusReader, r".*/wn-data-.*\.tab", encoding="utf8"), +) +english_wordnet: WordNetCorpusReader = LazyCorpusLoader( + # Latest Open English Wordnet + "english_wordnet", + WordNetCorpusReader, + LazyCorpusLoader("omw-2.0", CorpusReader, r".*/wn-data-.*\.tab", encoding="utf8"), +) +wordnet_ic: WordNetICCorpusReader = LazyCorpusLoader( + "wordnet_ic", WordNetICCorpusReader, r".*\.dat" +) +words: WordListCorpusReader = LazyCorpusLoader( + "words", WordListCorpusReader, r"(?!README|\.).*", encoding="ascii" +) + +# defined after treebank +propbank: PropbankCorpusReader = LazyCorpusLoader( + "propbank", + PropbankCorpusReader, + "prop.txt", + r"frames/.*\.xml", + "verbs.txt", + lambda filename: re.sub(r"^wsj/\d\d/", "", filename), + treebank, +) # Must be defined *after* treebank corpus. +nombank: NombankCorpusReader = LazyCorpusLoader( + "nombank.1.0", + NombankCorpusReader, + "nombank.1.0", + r"frames/.*\.xml", + "nombank.1.0.words", + lambda filename: re.sub(r"^wsj/\d\d/", "", filename), + treebank, +) # Must be defined *after* treebank corpus. +propbank_ptb: PropbankCorpusReader = LazyCorpusLoader( + "propbank", + PropbankCorpusReader, + "prop.txt", + r"frames/.*\.xml", + "verbs.txt", + lambda filename: filename.upper(), + ptb, +) # Must be defined *after* ptb corpus. +nombank_ptb: NombankCorpusReader = LazyCorpusLoader( + "nombank.1.0", + NombankCorpusReader, + "nombank.1.0", + r"frames/.*\.xml", + "nombank.1.0.words", + lambda filename: filename.upper(), + ptb, +) # Must be defined *after* ptb corpus. +semcor: SemcorCorpusReader = LazyCorpusLoader( + "semcor", SemcorCorpusReader, r"brown./tagfiles/br-.*\.xml", wordnet +) # Must be defined *after* wordnet corpus. + +nonbreaking_prefixes: NonbreakingPrefixesCorpusReader = LazyCorpusLoader( + "nonbreaking_prefixes", + NonbreakingPrefixesCorpusReader, + r"(?!README|\.).*", + encoding="utf8", +) +perluniprops: UnicharsCorpusReader = LazyCorpusLoader( + "perluniprops", + UnicharsCorpusReader, + r"(?!README|\.).*", + nltk_data_subdir="misc", + encoding="utf8", +) + +# mwa_ppdb = LazyCorpusLoader( +# 'mwa_ppdb', MWAPPDBCorpusReader, r'(?!README|\.).*', nltk_data_subdir='misc', encoding='utf8') + +# See https://github.com/nltk/nltk/issues/1579 +# and https://github.com/nltk/nltk/issues/1716 +# +# pl196x = LazyCorpusLoader( +# 'pl196x', Pl196xCorpusReader, r'[a-z]-.*\.xml', +# cat_file='cats.txt', textid_file='textids.txt', encoding='utf8') +# +# ipipan = LazyCorpusLoader( +# 'ipipan', IPIPANCorpusReader, r'(?!\.).*morph\.xml') +# +# nkjp = LazyCorpusLoader( +# 'nkjp', NKJPCorpusReader, r'', encoding='utf8') +# +# panlex_lite = LazyCorpusLoader( +# 'panlex_lite', PanLexLiteCorpusReader) +# +# ycoe = LazyCorpusLoader( +# 'ycoe', YCOECorpusReader) +# +# corpus not available with NLTK; these lines caused help(nltk.corpus) to break +# hebrew_treebank = LazyCorpusLoader( +# 'hebrew_treebank', BracketParseCorpusReader, r'.*\.txt') + + +# FIXME: override any imported demo from various corpora, see https://github.com/nltk/nltk/issues/2116 +def demo(): + # This is out-of-date: + abc.demo() + brown.demo() + # chat80.demo() + cmudict.demo() + conll2000.demo() + conll2002.demo() + genesis.demo() + gutenberg.demo() + ieer.demo() + inaugural.demo() + indian.demo() + names.demo() + ppattach.demo() + senseval.demo() + shakespeare.demo() + sinica_treebank.demo() + state_union.demo() + stopwords.demo() + timit.demo() + toolbox.demo() + treebank.demo() + udhr.demo() + webtext.demo() + words.demo() + + +# ycoe.demo() + +if __name__ == "__main__": + # demo() + pass diff --git a/nltk/corpus/europarl_raw.py b/nltk/corpus/europarl_raw.py new file mode 100644 index 0000000..7336400 --- /dev/null +++ b/nltk/corpus/europarl_raw.py @@ -0,0 +1,56 @@ +# Natural Language Toolkit: Europarl Corpus Readers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Nitin Madnani +# URL: +# For license information, see LICENSE.TXT + +import re + +from nltk.corpus.reader import * +from nltk.corpus.util import LazyCorpusLoader + +# Create a new corpus reader instance for each European language +danish: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/danish", EuroparlCorpusReader, r"ep-.*\.da", encoding="utf-8" +) + +dutch: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/dutch", EuroparlCorpusReader, r"ep-.*\.nl", encoding="utf-8" +) + +english: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/english", EuroparlCorpusReader, r"ep-.*\.en", encoding="utf-8" +) + +finnish: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/finnish", EuroparlCorpusReader, r"ep-.*\.fi", encoding="utf-8" +) + +french: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/french", EuroparlCorpusReader, r"ep-.*\.fr", encoding="utf-8" +) + +german: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/german", EuroparlCorpusReader, r"ep-.*\.de", encoding="utf-8" +) + +greek: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/greek", EuroparlCorpusReader, r"ep-.*\.el", encoding="utf-8" +) + +italian: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/italian", EuroparlCorpusReader, r"ep-.*\.it", encoding="utf-8" +) + +portuguese: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/portuguese", EuroparlCorpusReader, r"ep-.*\.pt", encoding="utf-8" +) + +spanish: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/spanish", EuroparlCorpusReader, r"ep-.*\.es", encoding="utf-8" +) + +swedish: EuroparlCorpusReader = LazyCorpusLoader( + "europarl_raw/swedish", EuroparlCorpusReader, r"ep-.*\.sv", encoding="utf-8" +) diff --git a/nltk/corpus/reader/__init__.py b/nltk/corpus/reader/__init__.py new file mode 100644 index 0000000..690eb60 --- /dev/null +++ b/nltk/corpus/reader/__init__.py @@ -0,0 +1,186 @@ +# Natural Language Toolkit: Corpus Readers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +NLTK corpus readers. The modules in this package provide functions +that can be used to read corpus fileids in a variety of formats. These +functions can be used to read both the corpus fileids that are +distributed in the NLTK corpus package, and corpus fileids that are part +of external corpora. + +Corpus Reader Functions +======================= +Each corpus module defines one or more "corpus reader functions", +which can be used to read documents from that corpus. These functions +take an argument, ``item``, which is used to indicate which document +should be read from the corpus: + +- If ``item`` is one of the unique identifiers listed in the corpus + module's ``items`` variable, then the corresponding document will + be loaded from the NLTK corpus package. +- If ``item`` is a fileid, then that file will be read. + +Additionally, corpus reader functions can be given lists of item +names; in which case, they will return a concatenation of the +corresponding documents. + +Corpus reader functions are named based on the type of information +they return. Some common examples, and their return types, are: + +- words(): list of str +- sents(): list of (list of str) +- paras(): list of (list of (list of str)) +- tagged_words(): list of (str,str) tuple +- tagged_sents(): list of (list of (str,str)) +- tagged_paras(): list of (list of (list of (str,str))) +- chunked_sents(): list of (Tree w/ (str,str) leaves) +- parsed_sents(): list of (Tree with str leaves) +- parsed_paras(): list of (list of (Tree with str leaves)) +- xml(): A single xml ElementTree +- raw(): unprocessed corpus contents + +For example, to read a list of the words in the Brown Corpus, use +``nltk.corpus.brown.words()``: + + >>> from nltk.corpus import brown + >>> print(", ".join(brown.words()[:6])) # only first 6 words + The, Fulton, County, Grand, Jury, said + +isort:skip_file +""" + +from nltk.corpus.reader.plaintext import * +from nltk.corpus.reader.util import * +from nltk.corpus.reader.api import * +from nltk.corpus.reader.tagged import * +from nltk.corpus.reader.cmudict import * +from nltk.corpus.reader.conll import * +from nltk.corpus.reader.chunked import * +from nltk.corpus.reader.wordlist import * +from nltk.corpus.reader.xmldocs import * +from nltk.corpus.reader.ppattach import * +from nltk.corpus.reader.senseval import * +from nltk.corpus.reader.ieer import * +from nltk.corpus.reader.sinica_treebank import * +from nltk.corpus.reader.bracket_parse import * +from nltk.corpus.reader.indian import * +from nltk.corpus.reader.toolbox import * +from nltk.corpus.reader.timit import * +from nltk.corpus.reader.ycoe import * +from nltk.corpus.reader.rte import * +from nltk.corpus.reader.string_category import * +from nltk.corpus.reader.propbank import * +from nltk.corpus.reader.verbnet import * +from nltk.corpus.reader.bnc import * +from nltk.corpus.reader.nps_chat import * +from nltk.corpus.reader.wordnet import * +from nltk.corpus.reader.switchboard import * +from nltk.corpus.reader.dependency import * +from nltk.corpus.reader.nombank import * +from nltk.corpus.reader.ipipan import * +from nltk.corpus.reader.pl196x import * +from nltk.corpus.reader.knbc import * +from nltk.corpus.reader.chasen import * +from nltk.corpus.reader.childes import * +from nltk.corpus.reader.aligned import * +from nltk.corpus.reader.lin import * +from nltk.corpus.reader.semcor import * +from nltk.corpus.reader.framenet import * +from nltk.corpus.reader.udhr import * +from nltk.corpus.reader.bnc import * +from nltk.corpus.reader.sentiwordnet import * +from nltk.corpus.reader.twitter import * +from nltk.corpus.reader.nkjp import * +from nltk.corpus.reader.crubadan import * +from nltk.corpus.reader.mte import * +from nltk.corpus.reader.reviews import * +from nltk.corpus.reader.opinion_lexicon import * +from nltk.corpus.reader.pros_cons import * +from nltk.corpus.reader.categorized_sents import * +from nltk.corpus.reader.comparative_sents import * +from nltk.corpus.reader.panlex_lite import * +from nltk.corpus.reader.panlex_swadesh import * +from nltk.corpus.reader.bcp47 import * + +# Make sure that nltk.corpus.reader.bracket_parse gives the module, not +# the function bracket_parse() defined in nltk.tree: +from nltk.corpus.reader import bracket_parse + +__all__ = [ + "CorpusReader", + "CategorizedCorpusReader", + "PlaintextCorpusReader", + "find_corpus_fileids", + "TaggedCorpusReader", + "CMUDictCorpusReader", + "ConllChunkCorpusReader", + "WordListCorpusReader", + "PPAttachmentCorpusReader", + "SensevalCorpusReader", + "IEERCorpusReader", + "ChunkedCorpusReader", + "SinicaTreebankCorpusReader", + "BracketParseCorpusReader", + "IndianCorpusReader", + "ToolboxCorpusReader", + "TimitCorpusReader", + "YCOECorpusReader", + "MacMorphoCorpusReader", + "SyntaxCorpusReader", + "AlpinoCorpusReader", + "RTECorpusReader", + "StringCategoryCorpusReader", + "EuroparlCorpusReader", + "CategorizedBracketParseCorpusReader", + "CategorizedTaggedCorpusReader", + "CategorizedPlaintextCorpusReader", + "PortugueseCategorizedPlaintextCorpusReader", + "tagged_treebank_para_block_reader", + "PropbankCorpusReader", + "VerbnetCorpusReader", + "BNCCorpusReader", + "ConllCorpusReader", + "XMLCorpusReader", + "NPSChatCorpusReader", + "SwadeshCorpusReader", + "WordNetCorpusReader", + "WordNetICCorpusReader", + "SwitchboardCorpusReader", + "DependencyCorpusReader", + "NombankCorpusReader", + "IPIPANCorpusReader", + "Pl196xCorpusReader", + "TEICorpusView", + "KNBCorpusReader", + "ChasenCorpusReader", + "CHILDESCorpusReader", + "AlignedCorpusReader", + "TimitTaggedCorpusReader", + "LinThesaurusCorpusReader", + "SemcorCorpusReader", + "FramenetCorpusReader", + "UdhrCorpusReader", + "BNCCorpusReader", + "SentiWordNetCorpusReader", + "SentiSynset", + "TwitterCorpusReader", + "NKJPCorpusReader", + "CrubadanCorpusReader", + "MTECorpusReader", + "ReviewsCorpusReader", + "OpinionLexiconCorpusReader", + "ProsConsCorpusReader", + "CategorizedSentencesCorpusReader", + "ComparativeSentencesCorpusReader", + "PanLexLiteCorpusReader", + "NonbreakingPrefixesCorpusReader", + "UnicharsCorpusReader", + "MWAPPDBCorpusReader", + "PanlexSwadeshCorpusReader", + "BCP47CorpusReader", +] diff --git a/nltk/corpus/reader/aligned.py b/nltk/corpus/reader/aligned.py new file mode 100644 index 0000000..9c4fa33 --- /dev/null +++ b/nltk/corpus/reader/aligned.py @@ -0,0 +1,154 @@ +# Natural Language Toolkit: Aligned Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# Author: Steven Bird +# For license information, see LICENSE.TXT + +from nltk.corpus.reader.api import CorpusReader +from nltk.corpus.reader.util import ( + StreamBackedCorpusView, + concat, + read_alignedsent_block, +) +from nltk.tokenize import RegexpTokenizer, WhitespaceTokenizer +from nltk.translate import AlignedSent, Alignment + + +class AlignedCorpusReader(CorpusReader): + """ + Reader for corpora of word-aligned sentences. Tokens are assumed + to be separated by whitespace. Sentences begin on separate lines. + """ + + def __init__( + self, + root, + fileids, + sep="/", + word_tokenizer=WhitespaceTokenizer(), + sent_tokenizer=RegexpTokenizer("\n", gaps=True), + alignedsent_block_reader=read_alignedsent_block, + encoding="latin1", + ): + """ + Construct a new Aligned Corpus reader for a set of documents + located at the given root directory. Example usage: + + >>> root = '/...path to corpus.../' + >>> reader = AlignedCorpusReader(root, '.*', '.txt') # doctest: +SKIP + + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + """ + CorpusReader.__init__(self, root, fileids, encoding) + self._sep = sep + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + self._alignedsent_block_reader = alignedsent_block_reader + + def words(self, fileids=None): + """ + :return: the given file(s) as a list of words + and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + AlignedSentCorpusView( + fileid, + enc, + False, + False, + self._word_tokenizer, + self._sent_tokenizer, + self._alignedsent_block_reader, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + """ + :return: the given file(s) as a list of + sentences or utterances, each encoded as a list of word + strings. + :rtype: list(list(str)) + """ + return concat( + [ + AlignedSentCorpusView( + fileid, + enc, + False, + True, + self._word_tokenizer, + self._sent_tokenizer, + self._alignedsent_block_reader, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def aligned_sents(self, fileids=None): + """ + :return: the given file(s) as a list of AlignedSent objects. + :rtype: list(AlignedSent) + """ + return concat( + [ + AlignedSentCorpusView( + fileid, + enc, + True, + True, + self._word_tokenizer, + self._sent_tokenizer, + self._alignedsent_block_reader, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + +class AlignedSentCorpusView(StreamBackedCorpusView): + """ + A specialized corpus view for aligned sentences. + ``AlignedSentCorpusView`` objects are typically created by + ``AlignedCorpusReader`` (not directly by nltk users). + """ + + def __init__( + self, + corpus_file, + encoding, + aligned, + group_by_sent, + word_tokenizer, + sent_tokenizer, + alignedsent_block_reader, + ): + self._aligned = aligned + self._group_by_sent = group_by_sent + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + self._alignedsent_block_reader = alignedsent_block_reader + StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) + + def read_block(self, stream): + block = [ + self._word_tokenizer.tokenize(sent_str) + for alignedsent_str in self._alignedsent_block_reader(stream) + for sent_str in self._sent_tokenizer.tokenize(alignedsent_str) + ] + if self._aligned: + block[2] = Alignment.fromstring( + " ".join(block[2]) + ) # kludge; we shouldn't have tokenized the alignment string + block = [AlignedSent(*block)] + elif self._group_by_sent: + block = [block[0]] + else: + block = block[0] + + return block diff --git a/nltk/corpus/reader/api.py b/nltk/corpus/reader/api.py new file mode 100644 index 0000000..5d8fbf1 --- /dev/null +++ b/nltk/corpus/reader/api.py @@ -0,0 +1,529 @@ +# Natural Language Toolkit: API for Corpus Readers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +API for corpus readers. +""" + +import os +import re +from collections import defaultdict +from itertools import chain + +from nltk.corpus.reader.util import * +from nltk.data import FileSystemPathPointer, PathPointer, ZipFilePathPointer + + +class CorpusReader: + """ + A base class for "corpus reader" classes, each of which can be + used to read a specific corpus format. Each individual corpus + reader instance is used to read a specific corpus, consisting of + one or more files under a common root directory. Each file is + identified by its ``file identifier``, which is the relative path + to the file from the root directory. + + A separate subclass is defined for each corpus format. These + subclasses define one or more methods that provide 'views' on the + corpus contents, such as ``words()`` (for a list of words) and + ``parsed_sents()`` (for a list of parsed sentences). Called with + no arguments, these methods will return the contents of the entire + corpus. For most corpora, these methods define one or more + selection arguments, such as ``fileids`` or ``categories``, which can + be used to select which portion of the corpus should be returned. + """ + + def __init__(self, root, fileids, encoding="utf8", tagset=None): + """ + :type root: PathPointer or str + :param root: A path pointer identifying the root directory for + this corpus. If a string is specified, then it will be + converted to a ``PathPointer`` automatically. + :param fileids: A list of the files that make up this corpus. + This list can either be specified explicitly, as a list of + strings; or implicitly, as a regular expression over file + paths. The absolute path for each file will be constructed + by joining the reader's root to each file name. + :param encoding: The default unicode encoding for the files + that make up the corpus. The value of ``encoding`` can be any + of the following: + + - A string: ``encoding`` is the encoding name for all files. + - A dictionary: ``encoding[file_id]`` is the encoding + name for the file whose identifier is ``file_id``. If + ``file_id`` is not in ``encoding``, then the file + contents will be processed using non-unicode byte strings. + - A list: ``encoding`` should be a list of ``(regexp, encoding)`` + tuples. The encoding for a file whose identifier is ``file_id`` + will be the ``encoding`` value for the first tuple whose + ``regexp`` matches the ``file_id``. If no tuple's ``regexp`` + matches the ``file_id``, the file contents will be processed + using non-unicode byte strings. + - None: the file contents of all files will be + processed using non-unicode byte strings. + :param tagset: The name of the tagset used by this corpus, to be used + for normalizing or converting the POS tags returned by the + ``tagged_...()`` methods. + """ + # Convert the root to a path pointer, if necessary. + if isinstance(root, str) and not isinstance(root, PathPointer): + m = re.match(r"(.*\.zip)/?(.*)$|", root) + zipfile, zipentry = m.groups() + if zipfile: + root = ZipFilePathPointer(zipfile, zipentry) + else: + root = FileSystemPathPointer(root) + elif not isinstance(root, PathPointer): + raise TypeError("CorpusReader: expected a string or a PathPointer") + + # If `fileids` is a regexp, then expand it. + if isinstance(fileids, str): + fileids = find_corpus_fileids(root, fileids) + + self._fileids = fileids + """A list of the relative paths for the fileids that make up + this corpus.""" + + self._root = root + """The root directory for this corpus.""" + + self._readme = "README" + self._license = "LICENSE" + self._citation = "citation.bib" + + # If encoding was specified as a list of regexps, then convert + # it to a dictionary. + if isinstance(encoding, list): + encoding_dict = {} + for fileid in self._fileids: + for x in encoding: + (regexp, enc) = x + if re.match(regexp, fileid): + encoding_dict[fileid] = enc + break + encoding = encoding_dict + + self._encoding = encoding + """The default unicode encoding for the fileids that make up + this corpus. If ``encoding`` is None, then the file + contents are processed using byte strings.""" + self._tagset = tagset + + def __repr__(self): + if isinstance(self._root, ZipFilePathPointer): + path = f"{self._root.zipfile.filename}/{self._root.entry}" + else: + path = "%s" % self._root.path + return f"<{self.__class__.__name__} in {path!r}>" + + def ensure_loaded(self): + """ + Load this corpus (if it has not already been loaded). This is + used by LazyCorpusLoader as a simple method that can be used to + make sure a corpus is loaded -- e.g., in case a user wants to + do help(some_corpus). + """ + pass # no need to actually do anything. + + def readme(self): + """ + Return the contents of the corpus README file, if it exists. + """ + with self.open(self._readme) as f: + return f.read() + + def license(self): + """ + Return the contents of the corpus LICENSE file, if it exists. + """ + with self.open(self._license) as f: + return f.read() + + def citation(self): + """ + Return the contents of the corpus citation.bib file, if it exists. + """ + with self.open(self._citation) as f: + return f.read() + + def fileids(self): + """ + Return a list of file identifiers for the fileids that make up + this corpus. + """ + return self._fileids + + def abspath(self, fileid): + """ + Return the absolute path for the given file. + + :type fileid: str + :param fileid: The file identifier for the file whose path + should be returned. + :rtype: PathPointer + """ + return self._root.join(fileid) + + def abspaths(self, fileids=None, include_encoding=False, include_fileid=False): + """ + Return a list of the absolute paths for all fileids in this corpus; + or for the given list of fileids, if specified. + + :type fileids: None or str or list + :param fileids: Specifies the set of fileids for which paths should + be returned. Can be None, for all fileids; a list of + file identifiers, for a specified set of fileids; or a single + file identifier, for a single file. Note that the return + value is always a list of paths, even if ``fileids`` is a + single file identifier. + + :param include_encoding: If true, then return a list of + ``(path_pointer, encoding)`` tuples. + + :rtype: list(PathPointer) + """ + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + paths = [self._root.join(f) for f in fileids] + + if include_encoding and include_fileid: + return list(zip(paths, [self.encoding(f) for f in fileids], fileids)) + elif include_fileid: + return list(zip(paths, fileids)) + elif include_encoding: + return list(zip(paths, [self.encoding(f) for f in fileids])) + else: + return paths + + def raw(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a single string. + :rtype: str + """ + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + contents = [] + for f in fileids: + with self.open(f) as fp: + contents.append(fp.read()) + return concat(contents) + + def open(self, file): + """ + Return an open stream for the given file. + Security patched: prevents path traversal and scoped escapes. + """ + # Layer 1: Lexical guard + if os.path.isabs(file) or ".." in file.replace("\\", "/"): + raise ValueError(f"CorpusReader paths must be relative: {file}") + + path = self._root.join(file) + + # Layer 2: Scoped resolved guard (Fixes symlink escape test) + from nltk.pathsec import validate_path + + validate_path(path, context="CorpusReader", required_root=self._root) + + # --- FIX: Handle dict-based encodings (e.g., UDHR corpus) --- + encoding = self._encoding + if isinstance(encoding, dict): + encoding = encoding.get(file) + + # Layer 3: Global sentinel check happens inside path.open() + return path.open(encoding=encoding) + + def encoding(self, file): + """ + Return the unicode encoding for the given corpus file, if known. + If the encoding is unknown, or if the given file should be + processed using byte strings (str), then return None. + """ + if isinstance(self._encoding, dict): + return self._encoding.get(file) + else: + return self._encoding + + def _get_root(self): + return self._root + + root = property( + _get_root, + doc=""" + The directory where this corpus is stored. + + :type: PathPointer""", + ) + + +###################################################################### +# { Corpora containing categorized items +###################################################################### + + +class CategorizedCorpusReader: + """ + A mixin class used to aid in the implementation of corpus readers + for categorized corpora. This class defines the method + ``categories()``, which returns a list of the categories for the + corpus or for a specified set of fileids; and overrides ``fileids()`` + to take a ``categories`` argument, restricting the set of fileids to + be returned. + + Subclasses are expected to: + + - Call ``__init__()`` to set up the mapping. + + - Override all view methods to accept a ``categories`` parameter, + which can be used *instead* of the ``fileids`` parameter, to + select which fileids should be included in the returned view. + """ + + def __init__(self, kwargs): + """ + Initialize this mapping based on keyword arguments, as + follows: + + - cat_pattern: A regular expression pattern used to find the + category for each file identifier. The pattern will be + applied to each file identifier, and the first matching + group will be used as the category label for that file. + + - cat_map: A dictionary, mapping from file identifiers to + category labels. + + - cat_file: The name of a file that contains the mapping + from file identifiers to categories. The argument + ``cat_delimiter`` can be used to specify a delimiter. + + The corresponding argument will be deleted from ``kwargs``. If + more than one argument is specified, an exception will be + raised. + """ + self._f2c = None #: file-to-category mapping + self._c2f = None #: category-to-file mapping + + self._pattern = None #: regexp specifying the mapping + self._map = None #: dict specifying the mapping + self._file = None #: fileid of file containing the mapping + self._delimiter = None #: delimiter for ``self._file`` + + if "cat_pattern" in kwargs: + self._pattern = kwargs["cat_pattern"] + del kwargs["cat_pattern"] + elif "cat_map" in kwargs: + self._map = kwargs["cat_map"] + del kwargs["cat_map"] + elif "cat_file" in kwargs: + self._file = kwargs["cat_file"] + del kwargs["cat_file"] + if "cat_delimiter" in kwargs: + self._delimiter = kwargs["cat_delimiter"] + del kwargs["cat_delimiter"] + else: + raise ValueError( + "Expected keyword argument cat_pattern or " "cat_map or cat_file." + ) + + if "cat_pattern" in kwargs or "cat_map" in kwargs or "cat_file" in kwargs: + raise ValueError( + "Specify exactly one of: cat_pattern, " "cat_map, cat_file." + ) + + def _init(self): + self._f2c = defaultdict(set) + self._c2f = defaultdict(set) + + if self._pattern is not None: + for file_id in self._fileids: + category = re.match(self._pattern, file_id).group(1) + self._add(file_id, category) + + elif self._map is not None: + for file_id, categories in self._map.items(): + for category in categories: + self._add(file_id, category) + + elif self._file is not None: + with self.open(self._file) as f: + for line in f.readlines(): + line = line.strip() + file_id, categories = line.split(self._delimiter, 1) + if file_id not in self.fileids(): + raise ValueError( + "In category mapping file %s: %s " + "not found" % (self._file, file_id) + ) + for category in categories.split(self._delimiter): + self._add(file_id, category) + + def _add(self, file_id, category): + self._f2c[file_id].add(category) + self._c2f[category].add(file_id) + + def categories(self, fileids=None): + """ + Return a list of the categories that are defined for this corpus, + or for the file(s) if it is given. + """ + if self._f2c is None: + self._init() + if fileids is None: + return sorted(self._c2f) + if isinstance(fileids, str): + fileids = [fileids] + return sorted(set.union(*(self._f2c[d] for d in fileids))) + + def fileids(self, categories=None): + """ + Return a list of file identifiers for the files that make up + this corpus, or that make up the given category(s) if specified. + """ + if categories is None: + return super().fileids() + elif isinstance(categories, str): + if self._f2c is None: + self._init() + if categories in self._c2f: + return sorted(self._c2f[categories]) + else: + raise ValueError("Category %s not found" % categories) + else: + if self._f2c is None: + self._init() + return sorted(set.union(*(self._c2f[c] for c in categories))) + + def _resolve(self, fileids, categories): + if fileids is not None and categories is not None: + raise ValueError("Specify fileids or categories, not both") + if categories is not None: + return self.fileids(categories) + else: + return fileids + + def raw(self, fileids=None, categories=None): + return super().raw(self._resolve(fileids, categories)) + + def words(self, fileids=None, categories=None): + return super().words(self._resolve(fileids, categories)) + + def sents(self, fileids=None, categories=None): + return super().sents(self._resolve(fileids, categories)) + + def paras(self, fileids=None, categories=None): + return super().paras(self._resolve(fileids, categories)) + + +###################################################################### +# { Treebank readers +###################################################################### + + +# [xx] is it worth it to factor this out? +class SyntaxCorpusReader(CorpusReader): + """ + An abstract base class for reading corpora consisting of + syntactically parsed text. Subclasses should define: + + - ``__init__``, which specifies the location of the corpus + and a method for detecting the sentence blocks in corpus files. + - ``_read_block``, which reads a block from the input stream. + - ``_word``, which takes a block and returns a list of list of words. + - ``_tag``, which takes a block and returns a list of list of tagged + words. + - ``_parse``, which takes a block and returns a list of parsed + sentences. + """ + + def _parse(self, s): + raise NotImplementedError() + + def _word(self, s): + raise NotImplementedError() + + def _tag(self, s): + raise NotImplementedError() + + def _read_block(self, stream): + raise NotImplementedError() + + def parsed_sents(self, fileids=None): + reader = self._read_parsed_sent_block + return concat( + [ + StreamBackedCorpusView(fileid, reader, encoding=enc) + for fileid, enc in self.abspaths(fileids, True) + ] + ) + + def tagged_sents(self, fileids=None, tagset=None): + def reader(stream): + return self._read_tagged_sent_block(stream, tagset) + + return concat( + [ + StreamBackedCorpusView(fileid, reader, encoding=enc) + for fileid, enc in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + reader = self._read_sent_block + return concat( + [ + StreamBackedCorpusView(fileid, reader, encoding=enc) + for fileid, enc in self.abspaths(fileids, True) + ] + ) + + def tagged_words(self, fileids=None, tagset=None): + def reader(stream): + return self._read_tagged_word_block(stream, tagset) + + return concat( + [ + StreamBackedCorpusView(fileid, reader, encoding=enc) + for fileid, enc in self.abspaths(fileids, True) + ] + ) + + def words(self, fileids=None): + return concat( + [ + StreamBackedCorpusView(fileid, self._read_word_block, encoding=enc) + for fileid, enc in self.abspaths(fileids, True) + ] + ) + + # ------------------------------------------------------------ + # { Block Readers + + def _read_word_block(self, stream): + return list(chain.from_iterable(self._read_sent_block(stream))) + + def _read_tagged_word_block(self, stream, tagset=None): + return list(chain.from_iterable(self._read_tagged_sent_block(stream, tagset))) + + def _read_sent_block(self, stream): + return list(filter(None, [self._word(t) for t in self._read_block(stream)])) + + def _read_tagged_sent_block(self, stream, tagset=None): + return list( + filter(None, [self._tag(t, tagset) for t in self._read_block(stream)]) + ) + + def _read_parsed_sent_block(self, stream): + return list(filter(None, [self._parse(t) for t in self._read_block(stream)])) + + # } End of Block Readers + # ------------------------------------------------------------ diff --git a/nltk/corpus/reader/bcp47.py b/nltk/corpus/reader/bcp47.py new file mode 100644 index 0000000..3444b07 --- /dev/null +++ b/nltk/corpus/reader/bcp47.py @@ -0,0 +1,218 @@ +# Natural Language Toolkit: BCP-47 language tags +# +# Copyright (C) 2022-2023 NLTK Project +# Author: Eric Kafe +# URL: +# For license information, see LICENSE.TXT + +import re +from warnings import warn +from xml.etree import ElementTree as et + +from nltk.corpus.reader import CorpusReader + + +class BCP47CorpusReader(CorpusReader): + """ + Parse BCP-47 composite language tags + + Supports all the main subtags, and the 'u-sd' extension: + + >>> from nltk.corpus import bcp47 + >>> bcp47.name('oc-gascon-u-sd-fr64') + 'Occitan (post 1500): Gascon: Pyrénées-Atlantiques' + + Can load a conversion table to Wikidata Q-codes: + >>> bcp47.load_wiki_q() + >>> bcp47.wiki_q['en-GI-spanglis'] + 'Q79388' + + """ + + def __init__(self, root, fileids): + """Read the BCP-47 database""" + super().__init__(root, fileids) + self.langcode = {} + with self.open("iana/language-subtag-registry.txt") as fp: + self.db = self.data_dict(fp.read().split("%%\n")) + with self.open("cldr/common-subdivisions-en.xml") as fp: + self.subdiv = self.subdiv_dict( + et.parse(fp).iterfind("localeDisplayNames/subdivisions/subdivision") + ) + self.morphology() + + def load_wiki_q(self): + """Load conversion table to Wikidata Q-codes (only if needed)""" + with self.open("cldr/tools-cldr-rdf-external-entityToCode.tsv") as fp: + self.wiki_q = self.wiki_dict(fp.read().strip().split("\n")[1:]) + + def wiki_dict(self, lines): + """Convert Wikidata list of Q-codes to a BCP-47 dictionary""" + return { + pair[1]: pair[0].split("/")[-1] + for pair in [line.strip().split("\t") for line in lines] + } + + def subdiv_dict(self, subdivs): + """Convert the CLDR subdivisions list to a dictionary""" + return {sub.attrib["type"]: sub.text for sub in subdivs} + + def morphology(self): + self.casing = { + "language": str.lower, + "extlang": str.lower, + "script": str.title, + "region": str.upper, + "variant": str.lower, + } + dig = "[0-9]" + low = "[a-z]" + up = "[A-Z]" + alnum = "[a-zA-Z0-9]" + self.format = { + "language": re.compile(f"{low*3}?"), + "extlang": re.compile(f"{low*3}"), + "script": re.compile(f"{up}{low*3}"), + "region": re.compile(f"({up*2})|({dig*3})"), + "variant": re.compile(f"{alnum*4}{(alnum+'?')*4}"), + "singleton": re.compile(f"{low}"), + } + + def data_dict(self, records): + """Convert the BCP-47 language subtag registry to a dictionary""" + self.version = records[0].replace("File-Date:", "").strip() + dic = {} + dic["deprecated"] = {} + for label in [ + "language", + "extlang", + "script", + "region", + "variant", + "redundant", + "grandfathered", + ]: + dic["deprecated"][label] = {} + for record in records[1:]: + fields = [field.split(": ") for field in record.strip().split("\n")] + typ = fields[0][1] + tag = fields[1][1] + if typ not in dic: + dic[typ] = {} + subfields = {} + for field in fields[2:]: + if len(field) == 2: + [key, val] = field + if key not in subfields: + subfields[key] = [val] + else: # multiple value + subfields[key].append(val) + else: # multiline field + subfields[key][-1] += " " + field[0].strip() + if ( + "Deprecated" not in record + and typ == "language" + and key == "Description" + ): + self.langcode[subfields[key][-1]] = tag + for key in subfields: + if len(subfields[key]) == 1: # single value + subfields[key] = subfields[key][0] + if "Deprecated" in record: + dic["deprecated"][typ][tag] = subfields + else: + dic[typ][tag] = subfields + return dic + + def val2str(self, val): + """Return only first value""" + if type(val) == list: + # val = "/".join(val) # Concatenate all values + val = val[0] + return val + + def lang2str(self, lg_record): + """Concatenate subtag values""" + name = f"{lg_record['language']}" + for label in ["extlang", "script", "region", "variant", "extension"]: + if label in lg_record: + name += f": {lg_record[label]}" + return name + + def parse_tag(self, tag): + """Convert a BCP-47 tag to a dictionary of labelled subtags""" + subtags = tag.split("-") + lang = {} + labels = ["language", "extlang", "script", "region", "variant", "variant"] + while subtags and labels: + subtag = subtags.pop(0) + found = False + while labels: + label = labels.pop(0) + subtag = self.casing[label](subtag) + if self.format[label].fullmatch(subtag): + if subtag in self.db[label]: + found = True + valstr = self.val2str(self.db[label][subtag]["Description"]) + if label == "variant" and label in lang: + lang[label] += ": " + valstr + else: + lang[label] = valstr + break + elif subtag in self.db["deprecated"][label]: + found = True + note = f"The {subtag!r} {label} code is deprecated" + if "Preferred-Value" in self.db["deprecated"][label][subtag]: + prefer = self.db["deprecated"][label][subtag][ + "Preferred-Value" + ] + note += f"', prefer '{self.val2str(prefer)}'" + lang[label] = self.val2str( + self.db["deprecated"][label][subtag]["Description"] + ) + warn(note) + break + if not found: + if subtag == "u" and subtags[0] == "sd": # CLDR regional subdivisions + sd = subtags[1] + if sd in self.subdiv: + ext = self.subdiv[sd] + else: + ext = f"" + else: # other extension subtags are not supported yet + ext = f"{subtag}{''.join(['-'+ext for ext in subtags])}".lower() + if not self.format["singleton"].fullmatch(subtag): + ext = f"" + warn(ext) + lang["extension"] = ext + subtags = [] + return lang + + def name(self, tag): + """ + Convert a BCP-47 tag to a colon-separated string of subtag names + + >>> from nltk.corpus import bcp47 + >>> bcp47.name('ca-Latn-ES-valencia') + 'Catalan: Latin: Spain: Valencian' + + """ + for label in ["redundant", "grandfathered"]: + val = None + if tag in self.db[label]: + val = f"{self.db[label][tag]['Description']}" + note = f"The {tag!r} code is {label}" + elif tag in self.db["deprecated"][label]: + val = f"{self.db['deprecated'][label][tag]['Description']}" + note = f"The {tag!r} code is {label} and deprecated" + if "Preferred-Value" in self.db["deprecated"][label][tag]: + prefer = self.db["deprecated"][label][tag]["Preferred-Value"] + note += f", prefer {self.val2str(prefer)!r}" + if val: + warn(note) + return val + try: + return self.lang2str(self.parse_tag(tag)) + except Exception: + warn(f"Tag {tag!r} was not recognized") + return None diff --git a/nltk/corpus/reader/bnc.py b/nltk/corpus/reader/bnc.py new file mode 100644 index 0000000..f7ad3e1 --- /dev/null +++ b/nltk/corpus/reader/bnc.py @@ -0,0 +1,268 @@ +# Natural Language Toolkit: Plaintext Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +"""Corpus reader for the XML version of the British National Corpus.""" + +from defusedxml.ElementTree import parse as safe_parse + +from nltk.corpus.reader.util import concat +from nltk.corpus.reader.xmldocs import XMLCorpusReader, XMLCorpusView + + +class BNCCorpusReader(XMLCorpusReader): + r"""Corpus reader for the XML version of the British National Corpus. + + For access to the complete XML data structure, use the ``xml()`` + method. For access to simple word lists and tagged word lists, use + ``words()``, ``sents()``, ``tagged_words()``, and ``tagged_sents()``. + + You can obtain the full version of the BNC corpus at + https://www.ota.ox.ac.uk/desc/2554 + + If you extracted the archive to a directory called `BNC`, then you can + instantiate the reader as:: + + BNCCorpusReader(root='BNC/Texts/', fileids=r'[A-K]/\w*/\w*\.xml') + + """ + + def __init__(self, root, fileids, lazy=True): + XMLCorpusReader.__init__(self, root, fileids) + self._lazy = lazy + + def words(self, fileids=None, strip_space=True, stem=False): + """ + :return: the given file(s) as a list of words + and punctuation symbols. + :rtype: list(str) + + :param strip_space: If true, then strip trailing spaces from + word tokens. Otherwise, leave the spaces on the tokens. + :param stem: If true, then use word stems instead of word strings. + """ + return self._views(fileids, False, None, strip_space, stem) + + def tagged_words(self, fileids=None, c5=False, strip_space=True, stem=False): + """ + :return: the given file(s) as a list of tagged + words and punctuation symbols, encoded as tuples + ``(word,tag)``. + :rtype: list(tuple(str,str)) + + :param c5: If true, then the tags used will be the more detailed + c5 tags. Otherwise, the simplified tags will be used. + :param strip_space: If true, then strip trailing spaces from + word tokens. Otherwise, leave the spaces on the tokens. + :param stem: If true, then use word stems instead of word strings. + """ + tag = "c5" if c5 else "pos" + return self._views(fileids, False, tag, strip_space, stem) + + def sents(self, fileids=None, strip_space=True, stem=False): + """ + :return: the given file(s) as a list of + sentences or utterances, each encoded as a list of word + strings. + :rtype: list(list(str)) + + :param strip_space: If true, then strip trailing spaces from + word tokens. Otherwise, leave the spaces on the tokens. + :param stem: If true, then use word stems instead of word strings. + """ + return self._views(fileids, True, None, strip_space, stem) + + def tagged_sents(self, fileids=None, c5=False, strip_space=True, stem=False): + """ + :return: the given file(s) as a list of + sentences, each encoded as a list of ``(word,tag)`` tuples. + :rtype: list(list(tuple(str,str))) + + :param c5: If true, then the tags used will be the more detailed + c5 tags. Otherwise, the simplified tags will be used. + :param strip_space: If true, then strip trailing spaces from + word tokens. Otherwise, leave the spaces on the tokens. + :param stem: If true, then use word stems instead of word strings. + """ + tag = "c5" if c5 else "pos" + return self._views( + fileids, sent=True, tag=tag, strip_space=strip_space, stem=stem + ) + + def _views(self, fileids=None, sent=False, tag=False, strip_space=True, stem=False): + """A helper function that instantiates BNCWordViews or the list of words/sentences.""" + f = BNCWordView if self._lazy else self._words + return concat( + [ + f(fileid, sent, tag, strip_space, stem) + for fileid in self.abspaths(fileids) + ] + ) + + def _words(self, fileid, bracket_sent, tag, strip_space, stem): + """ + Helper used to implement the view methods -- returns a list of + words or a list of sentences, optionally tagged. + + :param fileid: The name of the underlying file. + :param bracket_sent: If true, include sentence bracketing. + :param tag: The name of the tagset to use, or None for no tags. + :param strip_space: If true, strip spaces from word tokens. + :param stem: If true, then substitute stems for words. + """ + result = [] + + with fileid.open() as fp: + xmldoc = safe_parse(fp).getroot() + for xmlsent in xmldoc.findall(".//s"): + sent = [] + for xmlword in _all_xmlwords_in(xmlsent): + word = xmlword.text + if not word: + word = "" # fixes issue 337? + if strip_space or stem: + word = word.strip() + if stem: + word = xmlword.get("hw", word) + if tag == "c5": + word = (word, xmlword.get("c5")) + elif tag == "pos": + word = (word, xmlword.get("pos", xmlword.get("c5"))) + sent.append(word) + if bracket_sent: + result.append(BNCSentence(xmlsent.attrib["n"], sent)) + else: + result.extend(sent) + + assert None not in result + return result + + +def _all_xmlwords_in(elt, result=None): + if result is None: + result = [] + for child in elt: + if child.tag in ("c", "w"): + result.append(child) + else: + _all_xmlwords_in(child, result) + return result + + +class BNCSentence(list): + """ + A list of words, augmented by an attribute ``num`` used to record + the sentence identifier (the ``n`` attribute from the XML). + """ + + def __init__(self, num, items): + self.num = num + list.__init__(self, items) + + +class BNCWordView(XMLCorpusView): + """ + A stream backed corpus view specialized for use with the BNC corpus. + """ + + tags_to_ignore = { + "pb", + "gap", + "vocal", + "event", + "unclear", + "shift", + "pause", + "align", + } + """These tags are ignored. For their description refer to the + technical documentation, for example, + http://www.natcorp.ox.ac.uk/docs/URG/ref-vocal.html + + """ + + def __init__(self, fileid, sent, tag, strip_space, stem): + """ + :param fileid: The name of the underlying file. + :param sent: If true, include sentence bracketing. + :param tag: The name of the tagset to use, or None for no tags. + :param strip_space: If true, strip spaces from word tokens. + :param stem: If true, then substitute stems for words. + """ + if sent: + tagspec = ".*/s" + else: + tagspec = ".*/s/(.*/)?(c|w)" + self._sent = sent + self._tag = tag + self._strip_space = strip_space + self._stem = stem + + self.title = None #: Title of the document. + self.author = None #: Author of the document. + self.editor = None #: Editor + self.resps = None #: Statement of responsibility + + XMLCorpusView.__init__(self, fileid, tagspec) + + # Read in a tasty header. + self._open() + self.read_block(self._stream, ".*/teiHeader$", self.handle_header) + self.close() + + # Reset tag context. + self._tag_context = {0: ()} + + def handle_header(self, elt, context): + # Set up some metadata! + titles = elt.findall("titleStmt/title") + if titles: + self.title = "\n".join(title.text.strip() for title in titles) + + authors = elt.findall("titleStmt/author") + if authors: + self.author = "\n".join(author.text.strip() for author in authors) + + editors = elt.findall("titleStmt/editor") + if editors: + self.editor = "\n".join(editor.text.strip() for editor in editors) + + resps = elt.findall("titleStmt/respStmt") + if resps: + self.resps = "\n\n".join( + "\n".join(resp_elt.text.strip() for resp_elt in resp) for resp in resps + ) + + def handle_elt(self, elt, context): + if self._sent: + return self.handle_sent(elt) + else: + return self.handle_word(elt) + + def handle_word(self, elt): + word = elt.text + if not word: + word = "" # fixes issue 337? + if self._strip_space or self._stem: + word = word.strip() + if self._stem: + word = elt.get("hw", word) + if self._tag == "c5": + word = (word, elt.get("c5")) + elif self._tag == "pos": + word = (word, elt.get("pos", elt.get("c5"))) + return word + + def handle_sent(self, elt): + sent = [] + for child in elt: + if child.tag in ("mw", "hi", "corr", "trunc"): + sent += [self.handle_word(w) for w in child] + elif child.tag in ("w", "c"): + sent.append(self.handle_word(child)) + elif child.tag not in self.tags_to_ignore: + raise ValueError("Unexpected element %s" % child.tag) + return BNCSentence(elt.attrib["n"], sent) diff --git a/nltk/corpus/reader/bracket_parse.py b/nltk/corpus/reader/bracket_parse.py new file mode 100644 index 0000000..f172d1a --- /dev/null +++ b/nltk/corpus/reader/bracket_parse.py @@ -0,0 +1,274 @@ +# Natural Language Toolkit: Penn Treebank Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT +""" +Corpus reader for corpora that consist of parenthesis-delineated parse trees. +""" + +import sys + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tag import map_tag +from nltk.tree import Tree + +# we use [^\s()]+ instead of \S+? to avoid matching () +SORTTAGWRD = re.compile(r"\((\d+) ([^\s()]+) ([^\s()]+)\)") +TAGWORD = re.compile(r"\(([^\s()]+) ([^\s()]+)\)") +WORD = re.compile(r"\([^\s()]+ ([^\s()]+)\)") +EMPTY_BRACKETS = re.compile(r"\s*\(\s*\(") + +# Alpino word/category nodes are one-per-line XML elements. ``AlpinoCorpusReader`` +# parses each one by pulling its attributes out with a single linear scan instead +# of chaining several lazy ``.*?`` groups that rescan the line: the previous +# patterns backtracked quadratically on a long, malformed ``\n]`` class keep every match +# inside a single tag on a single line, so each line is scanned at most once. +ALPINO_NODE = re.compile( + r"^[ \t]*[^>\n]*?)(?P/?)>", re.MULTILINE +) +ALPINO_ATTR = re.compile(r'(\w+)="([^"]*)"') +# The old substitutions captured ``begin="(\d+)"``, ``pos="(\w+)"``, +# ``cat="(\w+)"`` and ``word="([^"]+)"``, i.e. they only converted a node when +# these fields had the expected shape (else the tag was left untouched). Keep +# those constraints so behaviour is byte-for-byte identical on malformed input -- +# in particular, ``ordered`` output must not emit a non-numeric ``begin`` that +# would then fail to match ``SORTTAGWRD`` and skew the tagging/ordering. +ALPINO_DIGITS = re.compile(r"\d+") +ALPINO_WORD = re.compile(r"\w+") + + +def _alpino_node_to_sexpr(match, ordered): + """Convert one Alpino ```` element to s-expression notation. + + A self-closing ```` is a leaf word node and becomes ``(pos word)`` + -- or ``(begin pos word)`` when ``ordered`` is set; an opening ```` + is a category node and becomes ``(cat``. Nodes whose fields do not have the + shape the old regexes required are returned unchanged so later substitutions + can handle them. + """ + attrs = dict(ALPINO_ATTR.findall(match.group("body"))) + if match.group("selfclose"): + pos, word = attrs.get("pos"), attrs.get("word") + if not word or not pos or not ALPINO_WORD.fullmatch(pos): + return match.group(0) + if ordered: + begin = attrs.get("begin") + if not begin or not ALPINO_DIGITS.fullmatch(begin): + return match.group(0) + return f"({begin} {pos} {word})" + return f"({pos} {word})" + cat = attrs.get("cat") + if not cat or not ALPINO_WORD.fullmatch(cat): + return match.group(0) + return f"({cat}" + + +class BracketParseCorpusReader(SyntaxCorpusReader): + """ + Reader for corpora that consist of parenthesis-delineated parse trees, + like those found in the "combined" section of the Penn Treebank, + e.g. "(S (NP (DT the) (JJ little) (NN dog)) (VP (VBD barked)))". + + """ + + def __init__( + self, + root, + fileids, + comment_char=None, + detect_blocks="unindented_paren", + encoding="utf8", + tagset=None, + ): + """ + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + :param comment_char: The character which can appear at the start of + a line to indicate that the rest of the line is a comment. + :param detect_blocks: The method that is used to find blocks + in the corpus; can be 'unindented_paren' (every unindented + parenthesis starts a new parse) or 'sexpr' (brackets are + matched). + :param tagset: The name of the tagset used by this corpus, to be used + for normalizing or converting the POS tags returned by the + ``tagged_...()`` methods. + """ + SyntaxCorpusReader.__init__(self, root, fileids, encoding) + self._comment_char = comment_char + self._detect_blocks = detect_blocks + self._tagset = tagset + + def _read_block(self, stream): + if self._detect_blocks == "sexpr": + return read_sexpr_block(stream, comment_char=self._comment_char) + elif self._detect_blocks == "blankline": + return read_blankline_block(stream) + elif self._detect_blocks == "unindented_paren": + # Tokens start with unindented left parens. + toks = read_regexp_block(stream, start_re=r"^\(") + # Strip any comments out of the tokens. + if self._comment_char: + toks = [ + re.sub("(?m)^%s.*" % re.escape(self._comment_char), "", tok) + for tok in toks + ] + return toks + else: + assert 0, "bad block type" + + def _normalize(self, t): + # Replace leaves of the form (!), (,), with (! !), (, ,) + t = re.sub(r"\((.)\)", r"(\1 \1)", t) + # Replace leaves of the form (tag word root) with (tag word) + t = re.sub(r"\(([^\s()]+) ([^\s()]+) [^\s()]+\)", r"(\1 \2)", t) + return t + + def _parse(self, t): + try: + tree = Tree.fromstring(self._normalize(t)) + # If there's an empty node at the top, strip it off + if tree.label() == "" and len(tree) == 1: + return tree[0] + else: + return tree + + except ValueError as e: + sys.stderr.write("Bad tree detected; trying to recover...\n") + # Try to recover, if we can: + if e.args == ("mismatched parens",): + for n in range(1, 5): + try: + v = Tree(self._normalize(t + ")" * n)) + sys.stderr.write( + " Recovered by adding %d close " "paren(s)\n" % n + ) + return v + except ValueError: + pass + # Try something else: + sys.stderr.write(" Recovered by returning a flat parse.\n") + # sys.stderr.write(' '.join(t.split())+'\n') + return Tree("S", self._tag(t)) + + def _tag(self, t, tagset=None): + tagged_sent = [(w, p) for (p, w) in TAGWORD.findall(self._normalize(t))] + if tagset and tagset != self._tagset: + tagged_sent = [ + (w, map_tag(self._tagset, tagset, p)) for (w, p) in tagged_sent + ] + return tagged_sent + + def _word(self, t): + return WORD.findall(self._normalize(t)) + + +class CategorizedBracketParseCorpusReader( + CategorizedCorpusReader, BracketParseCorpusReader +): + """ + A reader for parsed corpora whose documents are + divided into categories based on their file identifiers. + @author: Nathan Schneider + """ + + def __init__(self, *args, **kwargs): + """ + Initialize the corpus reader. Categorization arguments + (C{cat_pattern}, C{cat_map}, and C{cat_file}) are passed to + the L{CategorizedCorpusReader constructor + }. The remaining arguments + are passed to the L{BracketParseCorpusReader constructor + }. + """ + CategorizedCorpusReader.__init__(self, kwargs) + BracketParseCorpusReader.__init__(self, *args, **kwargs) + + def tagged_words(self, fileids=None, categories=None, tagset=None): + return super().tagged_words(self._resolve(fileids, categories), tagset) + + def tagged_sents(self, fileids=None, categories=None, tagset=None): + return super().tagged_sents(self._resolve(fileids, categories), tagset) + + def tagged_paras(self, fileids=None, categories=None, tagset=None): + return super().tagged_paras(self._resolve(fileids, categories), tagset) + + def parsed_words(self, fileids=None, categories=None): + return super().parsed_words(self._resolve(fileids, categories)) + + def parsed_sents(self, fileids=None, categories=None): + return super().parsed_sents(self._resolve(fileids, categories)) + + def parsed_paras(self, fileids=None, categories=None): + return super().parsed_paras(self._resolve(fileids, categories)) + + +class AlpinoCorpusReader(BracketParseCorpusReader): + """ + Reader for the Alpino Dutch Treebank. + This corpus has a lexical breakdown structure embedded, as read by `_parse` + Unfortunately this puts punctuation and some other words out of the sentence + order in the xml element tree. This is no good for `tag_` and `word_` + `_tag` and `_word` will be overridden to use a non-default new parameter 'ordered' + to the overridden _normalize function. The _parse function can then remain + untouched. + """ + + def __init__(self, root, encoding="ISO-8859-1", tagset=None): + BracketParseCorpusReader.__init__( + self, + root, + r"alpino\.xml", + detect_blocks="blankline", + encoding=encoding, + tagset=tagset, + ) + + def _normalize(self, t, ordered=False): + """Normalize the xml sentence element in t. + The sentence elements , although embedded in a few overall + xml elements, are separated by blank lines. That's how the reader can + deliver them one at a time. + Each sentence has a few category subnodes that are of no use to us. + The remaining word nodes may or may not appear in the proper order. + Each word node has attributes, among which: + - begin : the position of the word in the sentence + - pos : Part of Speech: the Tag + - word : the actual word + The return value is a string with all xml elementes replaced by + clauses: either a cat clause with nested clauses, or a word clause. + The order of the bracket clauses closely follows the xml. + If ordered == True, the word clauses include an order sequence number. + If ordered == False, the word clauses only have pos and word parts. + """ + if t[:10] != "", r")", t) + t = re.sub(r".*", r"", t) + t = re.sub(r"", r"", t) + return t + + def _tag(self, t, tagset=None): + tagged_sent = [ + (int(o), w, p) + for (o, p, w) in SORTTAGWRD.findall(self._normalize(t, ordered=True)) + ] + tagged_sent.sort() + if tagset and tagset != self._tagset: + tagged_sent = [ + (w, map_tag(self._tagset, tagset, p)) for (o, w, p) in tagged_sent + ] + else: + tagged_sent = [(w, p) for (o, w, p) in tagged_sent] + return tagged_sent + + def _word(self, t): + """Return a correctly ordered list if words""" + tagged_sent = self._tag(t) + return [w for (w, p) in tagged_sent] diff --git a/nltk/corpus/reader/categorized_sents.py b/nltk/corpus/reader/categorized_sents.py new file mode 100644 index 0000000..31f2e01 --- /dev/null +++ b/nltk/corpus/reader/categorized_sents.py @@ -0,0 +1,168 @@ +# Natural Language Toolkit: Categorized Sentences Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +CorpusReader structured for corpora that contain one instance on each row. +This CorpusReader is specifically used for the Subjectivity Dataset and the +Sentence Polarity Dataset. + +- Subjectivity Dataset information - + +Authors: Bo Pang and Lillian Lee. +Url: https://www.cs.cornell.edu/people/pabo/movie-review-data + +Distributed with permission. + +Related papers: + +- Bo Pang and Lillian Lee. "A Sentimental Education: Sentiment Analysis Using + Subjectivity Summarization Based on Minimum Cuts". Proceedings of the ACL, + 2004. + +- Sentence Polarity Dataset information - + +Authors: Bo Pang and Lillian Lee. +Url: https://www.cs.cornell.edu/people/pabo/movie-review-data + +Related papers: + +- Bo Pang and Lillian Lee. "Seeing stars: Exploiting class relationships for + sentiment categorization with respect to rating scales". Proceedings of the + ACL, 2005. +""" + +from nltk.corpus.reader.api import * +from nltk.tokenize import * + + +class CategorizedSentencesCorpusReader(CategorizedCorpusReader, CorpusReader): + """ + A reader for corpora in which each row represents a single instance, mainly + a sentence. Istances are divided into categories based on their file identifiers + (see CategorizedCorpusReader). + Since many corpora allow rows that contain more than one sentence, it is + possible to specify a sentence tokenizer to retrieve all sentences instead + than all rows. + + Examples using the Subjectivity Dataset: + + >>> from nltk.corpus import subjectivity + >>> subjectivity.sents()[23] # doctest: +NORMALIZE_WHITESPACE + ['television', 'made', 'him', 'famous', ',', 'but', 'his', 'biggest', 'hits', + 'happened', 'off', 'screen', '.'] + >>> subjectivity.categories() + ['obj', 'subj'] + >>> subjectivity.words(categories='subj') + ['smart', 'and', 'alert', ',', 'thirteen', ...] + + Examples using the Sentence Polarity Dataset: + + >>> from nltk.corpus import sentence_polarity + >>> sentence_polarity.sents() # doctest: +NORMALIZE_WHITESPACE + [['simplistic', ',', 'silly', 'and', 'tedious', '.'], ["it's", 'so', 'laddish', + 'and', 'juvenile', ',', 'only', 'teenage', 'boys', 'could', 'possibly', 'find', + 'it', 'funny', '.'], ...] + >>> sentence_polarity.categories() + ['neg', 'pos'] + """ + + CorpusView = StreamBackedCorpusView + + def __init__( + self, + root, + fileids, + word_tokenizer=WhitespaceTokenizer(), + sent_tokenizer=None, + encoding="utf8", + **kwargs + ): + """ + :param root: The root directory for the corpus. + :param fileids: a list or regexp specifying the fileids in the corpus. + :param word_tokenizer: a tokenizer for breaking sentences or paragraphs + into words. Default: `WhitespaceTokenizer` + :param sent_tokenizer: a tokenizer for breaking paragraphs into sentences. + :param encoding: the encoding that should be used to read the corpus. + :param kwargs: additional parameters passed to CategorizedCorpusReader. + """ + + CorpusReader.__init__(self, root, fileids, encoding) + CategorizedCorpusReader.__init__(self, kwargs) + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + + def sents(self, fileids=None, categories=None): + """ + Return all sentences in the corpus or in the specified file(s). + + :param fileids: a list or regexp specifying the ids of the files whose + sentences have to be returned. + :param categories: a list specifying the categories whose sentences have + to be returned. + :return: the given file(s) as a list of sentences. + Each sentence is tokenized using the specified word_tokenizer. + :rtype: list(list(str)) + """ + fileids = self._resolve(fileids, categories) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(path, self._read_sent_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def words(self, fileids=None, categories=None): + """ + Return all words and punctuation symbols in the corpus or in the specified + file(s). + + :param fileids: a list or regexp specifying the ids of the files whose + words have to be returned. + :param categories: a list specifying the categories whose words have to + be returned. + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + fileids = self._resolve(fileids, categories) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(path, self._read_word_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def _read_sent_block(self, stream): + sents = [] + for i in range(20): # Read 20 lines at a time. + line = stream.readline() + if not line: + continue + if self._sent_tokenizer: + sents.extend( + [ + self._word_tokenizer.tokenize(sent) + for sent in self._sent_tokenizer.tokenize(line) + ] + ) + else: + sents.append(self._word_tokenizer.tokenize(line)) + return sents + + def _read_word_block(self, stream): + words = [] + for sent in self._read_sent_block(stream): + words.extend(sent) + return words diff --git a/nltk/corpus/reader/chasen.py b/nltk/corpus/reader/chasen.py new file mode 100644 index 0000000..188d0f8 --- /dev/null +++ b/nltk/corpus/reader/chasen.py @@ -0,0 +1,154 @@ +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Masato Hagiwara +# URL: +# For license information, see LICENSE.TXT + +import sys + +from nltk.corpus.reader import util +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * + + +class ChasenCorpusReader(CorpusReader): + def __init__(self, root, fileids, encoding="utf8", sent_splitter=None): + self._sent_splitter = sent_splitter + CorpusReader.__init__(self, root, fileids, encoding) + + def words(self, fileids=None): + return concat( + [ + ChasenCorpusView(fileid, enc, False, False, False, self._sent_splitter) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_words(self, fileids=None): + return concat( + [ + ChasenCorpusView(fileid, enc, True, False, False, self._sent_splitter) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + return concat( + [ + ChasenCorpusView(fileid, enc, False, True, False, self._sent_splitter) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_sents(self, fileids=None): + return concat( + [ + ChasenCorpusView(fileid, enc, True, True, False, self._sent_splitter) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def paras(self, fileids=None): + return concat( + [ + ChasenCorpusView(fileid, enc, False, True, True, self._sent_splitter) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_paras(self, fileids=None): + return concat( + [ + ChasenCorpusView(fileid, enc, True, True, True, self._sent_splitter) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + +class ChasenCorpusView(StreamBackedCorpusView): + """ + A specialized corpus view for ChasenReader. Similar to ``TaggedCorpusView``, + but this'll use fixed sets of word and sentence tokenizer. + """ + + def __init__( + self, + corpus_file, + encoding, + tagged, + group_by_sent, + group_by_para, + sent_splitter=None, + ): + self._tagged = tagged + self._group_by_sent = group_by_sent + self._group_by_para = group_by_para + self._sent_splitter = sent_splitter + StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) + + def read_block(self, stream): + """Reads one paragraph at a time.""" + block = [] + for para_str in read_regexp_block(stream, r".", r"^EOS\n"): + para = [] + + sent = [] + for line in para_str.splitlines(): + _eos = line.strip() == "EOS" + _cells = line.split("\t") + w = (_cells[0], "\t".join(_cells[1:])) + if not _eos: + sent.append(w) + + if _eos or (self._sent_splitter and self._sent_splitter(w)): + if not self._tagged: + sent = [w for (w, t) in sent] + if self._group_by_sent: + para.append(sent) + else: + para.extend(sent) + sent = [] + + if len(sent) > 0: + if not self._tagged: + sent = [w for (w, t) in sent] + + if self._group_by_sent: + para.append(sent) + else: + para.extend(sent) + + if self._group_by_para: + block.append(para) + else: + block.extend(para) + + return block + + +def demo(): + import nltk + from nltk.corpus.util import LazyCorpusLoader + + jeita = LazyCorpusLoader("jeita", ChasenCorpusReader, r".*chasen", encoding="utf-8") + print("/".join(jeita.words()[22100:22140])) + + print( + "\nEOS\n".join( + "\n".join("{}/{}".format(w[0], w[1].split("\t")[2]) for w in sent) + for sent in jeita.tagged_sents()[2170:2173] + ) + ) + + +def test(): + from nltk.corpus.util import LazyCorpusLoader + + jeita = LazyCorpusLoader("jeita", ChasenCorpusReader, r".*chasen", encoding="utf-8") + + assert isinstance(jeita.tagged_words()[0][1], str) + + +if __name__ == "__main__": + demo() + test() diff --git a/nltk/corpus/reader/childes.py b/nltk/corpus/reader/childes.py new file mode 100644 index 0000000..11e44d8 --- /dev/null +++ b/nltk/corpus/reader/childes.py @@ -0,0 +1,649 @@ +# CHILDES XML Corpus Reader + +# Copyright (C) 2001-2026 NLTK Project +# Author: Tomonori Nagano +# Alexis Dimitriadis +# URL: +# For license information, see LICENSE.TXT + +""" +Corpus reader for the XML version of the CHILDES corpus. +""" + +__docformat__ = "epytext en" + +import re +from collections import defaultdict + +from defusedxml.ElementTree import parse as safe_parse + +from nltk.corpus.reader.util import concat +from nltk.corpus.reader.xmldocs import XMLCorpusReader +from nltk.util import LazyConcatenation, LazyMap, flatten + +# to resolve the namespace issue +NS = "http://www.talkbank.org/ns/talkbank" + + +class CHILDESCorpusReader(XMLCorpusReader): + """ + Corpus reader for the XML version of the CHILDES corpus. + The CHILDES corpus is available at ``https://childes.talkbank.org/``. The XML + version of CHILDES is located at ``https://childes.talkbank.org/data-xml/``. + Copy the needed parts of the CHILDES XML corpus into the NLTK data directory + (``nltk_data/corpora/CHILDES/``). + + For access to the file text use the usual nltk functions, + ``words()``, ``sents()``, ``tagged_words()`` and ``tagged_sents()``. + """ + + def __init__(self, root, fileids, lazy=True): + XMLCorpusReader.__init__(self, root, fileids) + self._lazy = lazy + + def words( + self, + fileids=None, + speaker="ALL", + stem=False, + relation=False, + strip_space=True, + replace=False, + ): + """ + :return: the given file(s) as a list of words + :rtype: list(str) + + :param speaker: If specified, select specific speaker(s) defined + in the corpus. Default is 'ALL' (all participants). Common choices + are 'CHI' (the child), 'MOT' (mother), ['CHI','MOT'] (exclude + researchers) + :param stem: If true, then use word stems instead of word strings. + :param relation: If true, then return tuples of (stem, index, + dependent_index) + :param strip_space: If true, then strip trailing spaces from word + tokens. Otherwise, leave the spaces on the tokens. + :param replace: If true, then use the replaced (intended) word instead + of the original word (e.g., 'wat' will be replaced with 'watch') + """ + sent = None + pos = False + if not self._lazy: + return [ + self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + for fileid in self.abspaths(fileids) + ] + + get_words = lambda fileid: self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + return LazyConcatenation(LazyMap(get_words, self.abspaths(fileids))) + + def tagged_words( + self, + fileids=None, + speaker="ALL", + stem=False, + relation=False, + strip_space=True, + replace=False, + ): + """ + :return: the given file(s) as a list of tagged + words and punctuation symbols, encoded as tuples + ``(word,tag)``. + :rtype: list(tuple(str,str)) + + :param speaker: If specified, select specific speaker(s) defined + in the corpus. Default is 'ALL' (all participants). Common choices + are 'CHI' (the child), 'MOT' (mother), ['CHI','MOT'] (exclude + researchers) + :param stem: If true, then use word stems instead of word strings. + :param relation: If true, then return tuples of (stem, index, + dependent_index) + :param strip_space: If true, then strip trailing spaces from word + tokens. Otherwise, leave the spaces on the tokens. + :param replace: If true, then use the replaced (intended) word instead + of the original word (e.g., 'wat' will be replaced with 'watch') + """ + sent = None + pos = True + if not self._lazy: + return [ + self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + for fileid in self.abspaths(fileids) + ] + + get_words = lambda fileid: self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + return LazyConcatenation(LazyMap(get_words, self.abspaths(fileids))) + + def sents( + self, + fileids=None, + speaker="ALL", + stem=False, + relation=None, + strip_space=True, + replace=False, + ): + """ + :return: the given file(s) as a list of sentences or utterances, each + encoded as a list of word strings. + :rtype: list(list(str)) + + :param speaker: If specified, select specific speaker(s) defined + in the corpus. Default is 'ALL' (all participants). Common choices + are 'CHI' (the child), 'MOT' (mother), ['CHI','MOT'] (exclude + researchers) + :param stem: If true, then use word stems instead of word strings. + :param relation: If true, then return tuples of ``(str,pos,relation_list)``. + If there is manually-annotated relation info, it will return + tuples of ``(str,pos,test_relation_list,str,pos,gold_relation_list)`` + :param strip_space: If true, then strip trailing spaces from word + tokens. Otherwise, leave the spaces on the tokens. + :param replace: If true, then use the replaced (intended) word instead + of the original word (e.g., 'wat' will be replaced with 'watch') + """ + sent = True + pos = False + if not self._lazy: + return [ + self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + for fileid in self.abspaths(fileids) + ] + + get_words = lambda fileid: self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + return LazyConcatenation(LazyMap(get_words, self.abspaths(fileids))) + + def tagged_sents( + self, + fileids=None, + speaker="ALL", + stem=False, + relation=None, + strip_space=True, + replace=False, + ): + """ + :return: the given file(s) as a list of + sentences, each encoded as a list of ``(word,tag)`` tuples. + :rtype: list(list(tuple(str,str))) + + :param speaker: If specified, select specific speaker(s) defined + in the corpus. Default is 'ALL' (all participants). Common choices + are 'CHI' (the child), 'MOT' (mother), ['CHI','MOT'] (exclude + researchers) + :param stem: If true, then use word stems instead of word strings. + :param relation: If true, then return tuples of ``(str,pos,relation_list)``. + If there is manually-annotated relation info, it will return + tuples of ``(str,pos,test_relation_list,str,pos,gold_relation_list)`` + :param strip_space: If true, then strip trailing spaces from word + tokens. Otherwise, leave the spaces on the tokens. + :param replace: If true, then use the replaced (intended) word instead + of the original word (e.g., 'wat' will be replaced with 'watch') + """ + sent = True + pos = True + if not self._lazy: + return [ + self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + for fileid in self.abspaths(fileids) + ] + + get_words = lambda fileid: self._get_words( + fileid, speaker, sent, stem, relation, pos, strip_space, replace + ) + return LazyConcatenation(LazyMap(get_words, self.abspaths(fileids))) + + def corpus(self, fileids=None): + """ + :return: the given file(s) as a dict of ``(corpus_property_key, value)`` + :rtype: list(dict) + """ + if not self._lazy: + return [self._get_corpus(fileid) for fileid in self.abspaths(fileids)] + return LazyMap(self._get_corpus, self.abspaths(fileids)) + + def _get_corpus(self, fileid): + results = dict() + with fileid.open() as fp: + xmldoc = safe_parse(fp).getroot() + for key, value in xmldoc.items(): + results[key] = value + return results + + def participants(self, fileids=None): + """ + :return: the given file(s) as a dict of + ``(participant_property_key, value)`` + :rtype: list(dict) + """ + if not self._lazy: + return [self._get_participants(fileid) for fileid in self.abspaths(fileids)] + return LazyMap(self._get_participants, self.abspaths(fileids)) + + def _get_participants(self, fileid): + # multidimensional dicts + def dictOfDicts(): + return defaultdict(dictOfDicts) + + with fileid.open() as fp: + xmldoc = safe_parse(fp).getroot() + # getting participants' data + pat = dictOfDicts() + for participant in xmldoc.findall( + f".//{{{NS}}}Participants/{{{NS}}}participant" + ): + for key, value in participant.items(): + pat[participant.get("id")][key] = value + return pat + + def age(self, fileids=None, speaker="CHI", month=False): + """ + :return: the given file(s) as string or int + :rtype: list or int + + :param month: If true, return months instead of year-month-date + """ + if not self._lazy: + return [ + self._get_age(fileid, speaker, month) + for fileid in self.abspaths(fileids) + ] + get_age = lambda fileid: self._get_age(fileid, speaker, month) + return LazyMap(get_age, self.abspaths(fileids)) + + def _get_age(self, fileid, speaker, month): + with fileid.open() as fp: + xmldoc = safe_parse(fp).getroot() + for pat in xmldoc.findall(f".//{{{NS}}}Participants/{{{NS}}}participant"): + try: + if pat.get("id") == speaker: + age = pat.get("age") + if month: + age = self.convert_age(age) + return age + # some files have missing (TypeError) or malformed (ValueError) age + # data; AttributeError is kept for backward compatibility + except (TypeError, AttributeError, ValueError) as e: + return None + + def convert_age(self, age_year): + "Calculate age in months from a string in CHILDES format" + m = re.match(r"P(\d+)Y(\d+)M?(\d?\d?)D?", age_year) + if m is None: + # A string that does not fit the CHILDES age shape would otherwise + # make ``m.group(1)`` raise a cryptic ``AttributeError`` out of this + # public helper (CWE-476); fail with a clear, catchable error. + raise ValueError( + f"Cannot convert age {age_year!r}: expected a CHILDES age string " + "of the form 'PY' with an optional 'M' and " + "'D', e.g. 'P2Y10M', 'P2Y10', or 'P2Y1M15D'" + ) + age_month = int(m.group(1)) * 12 + int(m.group(2)) + try: + if int(m.group(3)) > 15: + age_month += 1 + # some corpora don't have age information? + except ValueError as e: + pass + return age_month + + def MLU(self, fileids=None, speaker="CHI"): + """ + :return: the given file(s) as a floating number + :rtype: list(float) + """ + if not self._lazy: + return [ + self._getMLU(fileid, speaker=speaker) + for fileid in self.abspaths(fileids) + ] + get_MLU = lambda fileid: self._getMLU(fileid, speaker=speaker) + return LazyMap(get_MLU, self.abspaths(fileids)) + + def _getMLU(self, fileid, speaker): + sents = self._get_words( + fileid, + speaker=speaker, + sent=True, + stem=True, + relation=False, + pos=True, + strip_space=True, + replace=True, + ) + results = [] + lastSent = [] + numFillers = 0 + sentDiscount = 0 + for sent in sents: + posList = [pos for (word, pos) in sent] + # if any part of the sentence is intelligible + if any(pos == "unk" for pos in posList): + continue + # if the sentence is null + elif sent == []: + continue + # if the sentence is the same as the last sent + elif sent == lastSent: + continue + else: + results.append([word for (word, pos) in sent]) + # count number of fillers + if len({"co", None}.intersection(posList)) > 0: + numFillers += posList.count("co") + numFillers += posList.count(None) + sentDiscount += 1 + lastSent = sent + try: + thisWordList = flatten(results) + # count number of morphemes + # (e.g., 'read' = 1 morpheme but 'read-PAST' is 2 morphemes) + numWords = ( + len(flatten([word.split("-") for word in thisWordList])) - numFillers + ) + numSents = len(results) - sentDiscount + mlu = numWords / numSents + except ZeroDivisionError: + mlu = 0 + # return {'mlu':mlu,'wordNum':numWords,'sentNum':numSents} + return mlu + + def _get_words( + self, fileid, speaker, sent, stem, relation, pos, strip_space, replace + ): + if ( + isinstance(speaker, str) and speaker != "ALL" + ): # ensure we have a list of speakers + speaker = [speaker] + with fileid.open() as fp: + xmldoc = safe_parse(fp).getroot() + # processing each xml doc + results = [] + for xmlsent in xmldoc.findall(".//{%s}u" % NS): + sents = [] + # select speakers + if speaker == "ALL" or xmlsent.get("who") in speaker: + for xmlword in xmlsent.findall(".//{%s}w" % NS): + infl = None + suffixStem = None + suffixTag = None + # getting replaced words + if replace and xmlsent.find(f".//{{{NS}}}w/{{{NS}}}replacement"): + xmlword = xmlsent.find( + f".//{{{NS}}}w/{{{NS}}}replacement/{{{NS}}}w" + ) + elif replace and xmlsent.find(f".//{{{NS}}}w/{{{NS}}}wk"): + xmlword = xmlsent.find(f".//{{{NS}}}w/{{{NS}}}wk") + # get text + if xmlword.text: + word = xmlword.text + else: + word = "" + # strip tailing space + if strip_space: + word = word.strip() + # stem + if relation or stem: + try: + xmlstem = xmlword.find(".//{%s}stem" % NS) + word = xmlstem.text + except AttributeError as e: + pass + # if there is an inflection + try: + xmlinfl = xmlword.find( + f".//{{{NS}}}mor/{{{NS}}}mw/{{{NS}}}mk" + ) + word += "-" + xmlinfl.text + except Exception: + pass + # if there is a suffix + try: + xmlsuffix = xmlword.find( + ".//{%s}mor/{%s}mor-post/{%s}mw/{%s}stem" + % (NS, NS, NS, NS) + ) + suffixStem = xmlsuffix.text + except AttributeError: + suffixStem = "" + if suffixStem: + word += "~" + suffixStem + # pos + if relation or pos: + try: + xmlpos = xmlword.findall(".//{%s}c" % NS) + xmlpos2 = xmlword.findall(".//{%s}s" % NS) + if xmlpos2 != []: + tag = xmlpos[0].text + ":" + xmlpos2[0].text + else: + tag = xmlpos[0].text + except (AttributeError, IndexError) as e: + tag = "" + try: + xmlsuffixpos = xmlword.findall( + ".//{%s}mor/{%s}mor-post/{%s}mw/{%s}pos/{%s}c" + % (NS, NS, NS, NS, NS) + ) + xmlsuffixpos2 = xmlword.findall( + ".//{%s}mor/{%s}mor-post/{%s}mw/{%s}pos/{%s}s" + % (NS, NS, NS, NS, NS) + ) + if xmlsuffixpos2: + suffixTag = ( + xmlsuffixpos[0].text + ":" + xmlsuffixpos2[0].text + ) + else: + suffixTag = xmlsuffixpos[0].text + except Exception: + pass + if suffixTag: + tag += "~" + suffixTag + word = (word, tag) + # relational + # the gold standard is stored in + # + if relation: + for xmlstem_rel in xmlword.findall( + f".//{{{NS}}}mor/{{{NS}}}gra" + ): + if not xmlstem_rel.get("type") == "grt": + word = ( + word[0], + word[1], + xmlstem_rel.get("index") + + "|" + + xmlstem_rel.get("head") + + "|" + + xmlstem_rel.get("relation"), + ) + else: + word = ( + word[0], + word[1], + word[2], + word[0], + word[1], + xmlstem_rel.get("index") + + "|" + + xmlstem_rel.get("head") + + "|" + + xmlstem_rel.get("relation"), + ) + try: + for xmlpost_rel in xmlword.findall( + f".//{{{NS}}}mor/{{{NS}}}mor-post/{{{NS}}}gra" + ): + if not xmlpost_rel.get("type") == "grt": + suffixStem = ( + suffixStem[0], + suffixStem[1], + xmlpost_rel.get("index") + + "|" + + xmlpost_rel.get("head") + + "|" + + xmlpost_rel.get("relation"), + ) + else: + suffixStem = ( + suffixStem[0], + suffixStem[1], + suffixStem[2], + suffixStem[0], + suffixStem[1], + xmlpost_rel.get("index") + + "|" + + xmlpost_rel.get("head") + + "|" + + xmlpost_rel.get("relation"), + ) + except Exception: + pass + sents.append(word) + if sent or relation: + results.append(sents) + else: + results.extend(sents) + return LazyMap(lambda x: x, results) + + # Ready-to-use browser opener + + """ + The base URL for viewing files on the childes website. This + shouldn't need to be changed, unless CHILDES changes the configuration + of their server or unless the user sets up their own corpus webserver. + """ + childes_url_base = r"https://childes.talkbank.org/browser/index.php?url=" + + def webview_file(self, fileid, urlbase=None): + """Map a corpus file to its web version on the CHILDES website, + and open it in a web browser. + + The complete URL to be used is: + childes.childes_url_base + urlbase + fileid.replace('.xml', '.cha') + + If no urlbase is passed, we try to calculate it. This + requires that the childes corpus was set up to mirror the + folder hierarchy under childes.psy.cmu.edu/data-xml/, e.g.: + nltk_data/corpora/childes/Eng-USA/Cornell/??? or + nltk_data/corpora/childes/Romance/Spanish/Aguirre/??? + + The function first looks (as a special case) if "Eng-USA" is + on the path consisting of +fileid; then if + "childes", possibly followed by "data-xml", appears. If neither + one is found, we use the unmodified fileid and hope for the best. + If this is not right, specify urlbase explicitly, e.g., if the + corpus root points to the Cornell folder, urlbase='Eng-USA/Cornell'. + """ + + import webbrowser + + if urlbase: + path = urlbase + "/" + fileid + else: + full = self.root + "/" + fileid + full = re.sub(r"\\", "/", full) + if "/childes/" in full.lower(): + # Discard /data-xml/ if present + path = re.findall(r"(?i)/childes(?:/data-xml)?/(.*)\.xml", full)[0] + elif "eng-usa" in full.lower(): + path = "Eng-USA/" + re.findall(r"/(?i)Eng-USA/(.*)\.xml", full)[0] + else: + path = fileid + + # Strip ".xml" and add ".cha", as necessary: + if path.endswith(".xml"): + path = path[:-4] + + if not path.endswith(".cha"): + path = path + ".cha" + + url = self.childes_url_base + path + + webbrowser.open_new_tab(url) + print("Opening in browser:", url) + # Pausing is a good idea, but it's up to the user... + # raw_input("Hit Return to continue") + + +def demo(corpus_root=None): + """ + The CHILDES corpus should be manually downloaded and saved + to ``[NLTK_Data_Dir]/corpora/childes/`` + """ + if not corpus_root: + from nltk.data import find + + corpus_root = find("corpora/childes/data-xml/Eng-USA/") + + try: + childes = CHILDESCorpusReader(corpus_root, ".*.xml") + # describe all corpus + for file in childes.fileids()[:5]: + corpus = "" + corpus_id = "" + for key, value in childes.corpus(file)[0].items(): + if key == "Corpus": + corpus = value + if key == "Id": + corpus_id = value + print("Reading", corpus, corpus_id, " .....") + print("words:", childes.words(file)[:7], "...") + print( + "words with replaced words:", + childes.words(file, replace=True)[:7], + " ...", + ) + print("words with pos tags:", childes.tagged_words(file)[:7], " ...") + print("words (only MOT):", childes.words(file, speaker="MOT")[:7], "...") + print("words (only CHI):", childes.words(file, speaker="CHI")[:7], "...") + print("stemmed words:", childes.words(file, stem=True)[:7], " ...") + print( + "words with relations and pos-tag:", + childes.words(file, relation=True)[:5], + " ...", + ) + print("sentence:", childes.sents(file)[:2], " ...") + for participant, values in childes.participants(file)[0].items(): + for key, value in values.items(): + print("\tparticipant", participant, key, ":", value) + print("num of sent:", len(childes.sents(file))) + print("num of morphemes:", len(childes.words(file, stem=True))) + print("age:", childes.age(file)) + print("age in month:", childes.age(file, month=True)) + print("MLU:", childes.MLU(file)) + print() + + except LookupError as e: + print( + """The CHILDES corpus, or the parts you need, should be manually + downloaded from https://childes.talkbank.org/data-xml/ and saved at + [NLTK_Data_Dir]/corpora/childes/ + Alternately, you can call the demo with the path to a portion of the CHILDES corpus, e.g.: + demo('/path/to/childes/data-xml/Eng-USA/") + """ + ) + + # To test remote fetching securely, use the pathsec wrapper: + # from nltk.pathsec import urlopen, ZipFile + # corpus_root_http = urlopen('https://childes.talkbank.org/data-xml/Eng-USA/Bates.zip') + # corpus_root_http_bates = ZipFile(cStringIO.StringIO(corpus_root_http.read())) + ##this fails + # childes = CHILDESCorpusReader(corpus_root_http_bates,corpus_root_http_bates.namelist()) + + +if __name__ == "__main__": + demo() diff --git a/nltk/corpus/reader/chunked.py b/nltk/corpus/reader/chunked.py new file mode 100644 index 0000000..66aa81f --- /dev/null +++ b/nltk/corpus/reader/chunked.py @@ -0,0 +1,273 @@ +# Natural Language Toolkit: Chunked Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +A reader for corpora that contain chunked (and optionally tagged) +documents. +""" + +import codecs +import os.path + +import nltk +from nltk.chunk import tagstr2tree +from nltk.corpus.reader.api import * +from nltk.corpus.reader.bracket_parse import BracketParseCorpusReader +from nltk.corpus.reader.util import * +from nltk.tokenize import * +from nltk.tree import Tree + + +class ChunkedCorpusReader(CorpusReader): + """ + Reader for chunked (and optionally tagged) corpora. Paragraphs + are split using a block reader. They are then tokenized into + sentences using a sentence tokenizer. Finally, these sentences + are parsed into chunk trees using a string-to-chunktree conversion + function. Each of these steps can be performed using a default + function or a custom function. By default, paragraphs are split + on blank lines; sentences are listed one per line; and sentences + are parsed into chunk trees using ``nltk.chunk.tagstr2tree``. + """ + + def __init__( + self, + root, + fileids, + extension="", + str2chunktree=tagstr2tree, + sent_tokenizer=RegexpTokenizer("\n", gaps=True), + para_block_reader=read_blankline_block, + encoding="utf8", + tagset=None, + ): + """ + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + """ + CorpusReader.__init__(self, root, fileids, encoding) + self._cv_args = (str2chunktree, sent_tokenizer, para_block_reader, tagset) + """Arguments for corpus views generated by this corpus: a tuple + (str2chunktree, sent_tokenizer, para_block_tokenizer)""" + + def words(self, fileids=None): + """ + :return: the given file(s) as a list of words + and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + ChunkedCorpusView(f, enc, 0, 0, 0, 0, *self._cv_args) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + """ + :return: the given file(s) as a list of + sentences or utterances, each encoded as a list of word + strings. + :rtype: list(list(str)) + """ + return concat( + [ + ChunkedCorpusView(f, enc, 0, 1, 0, 0, *self._cv_args) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def paras(self, fileids=None): + """ + :return: the given file(s) as a list of + paragraphs, each encoded as a list of sentences, which are + in turn encoded as lists of word strings. + :rtype: list(list(list(str))) + """ + return concat( + [ + ChunkedCorpusView(f, enc, 0, 1, 1, 0, *self._cv_args) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_words(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of tagged + words and punctuation symbols, encoded as tuples + ``(word,tag)``. + :rtype: list(tuple(str,str)) + """ + return concat( + [ + ChunkedCorpusView( + f, enc, 1, 0, 0, 0, *self._cv_args, target_tagset=tagset + ) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_sents(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of + sentences, each encoded as a list of ``(word,tag)`` tuples. + + :rtype: list(list(tuple(str,str))) + """ + return concat( + [ + ChunkedCorpusView( + f, enc, 1, 1, 0, 0, *self._cv_args, target_tagset=tagset + ) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_paras(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of + paragraphs, each encoded as a list of sentences, which are + in turn encoded as lists of ``(word,tag)`` tuples. + :rtype: list(list(list(tuple(str,str)))) + """ + return concat( + [ + ChunkedCorpusView( + f, enc, 1, 1, 1, 0, *self._cv_args, target_tagset=tagset + ) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def chunked_words(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of tagged + words and chunks. Words are encoded as ``(word, tag)`` + tuples (if the corpus has tags) or word strings (if the + corpus has no tags). Chunks are encoded as depth-one + trees over ``(word,tag)`` tuples or word strings. + :rtype: list(tuple(str,str) and Tree) + """ + return concat( + [ + ChunkedCorpusView( + f, enc, 1, 0, 0, 1, *self._cv_args, target_tagset=tagset + ) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def chunked_sents(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of + sentences, each encoded as a shallow Tree. The leaves + of these trees are encoded as ``(word, tag)`` tuples (if + the corpus has tags) or word strings (if the corpus has no + tags). + :rtype: list(Tree) + """ + return concat( + [ + ChunkedCorpusView( + f, enc, 1, 1, 0, 1, *self._cv_args, target_tagset=tagset + ) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def chunked_paras(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of + paragraphs, each encoded as a list of sentences, which are + in turn encoded as a shallow Tree. The leaves of these + trees are encoded as ``(word, tag)`` tuples (if the corpus + has tags) or word strings (if the corpus has no tags). + :rtype: list(list(Tree)) + """ + return concat( + [ + ChunkedCorpusView( + f, enc, 1, 1, 1, 1, *self._cv_args, target_tagset=tagset + ) + for (f, enc) in self.abspaths(fileids, True) + ] + ) + + def _read_block(self, stream): + return [tagstr2tree(t) for t in read_blankline_block(stream)] + + +class ChunkedCorpusView(StreamBackedCorpusView): + def __init__( + self, + fileid, + encoding, + tagged, + group_by_sent, + group_by_para, + chunked, + str2chunktree, + sent_tokenizer, + para_block_reader, + source_tagset=None, + target_tagset=None, + ): + StreamBackedCorpusView.__init__(self, fileid, encoding=encoding) + self._tagged = tagged + self._group_by_sent = group_by_sent + self._group_by_para = group_by_para + self._chunked = chunked + self._str2chunktree = str2chunktree + self._sent_tokenizer = sent_tokenizer + self._para_block_reader = para_block_reader + self._source_tagset = source_tagset + self._target_tagset = target_tagset + + def read_block(self, stream): + block = [] + for para_str in self._para_block_reader(stream): + para = [] + for sent_str in self._sent_tokenizer.tokenize(para_str): + sent = self._str2chunktree( + sent_str, + source_tagset=self._source_tagset, + target_tagset=self._target_tagset, + ) + + # If requested, throw away the tags. + if not self._tagged: + sent = self._untag(sent) + + # If requested, throw away the chunks. + if not self._chunked: + sent = sent.leaves() + + # Add the sentence to `para`. + if self._group_by_sent: + para.append(sent) + else: + para.extend(sent) + + # Add the paragraph to `block`. + if self._group_by_para: + block.append(para) + else: + block.extend(para) + + # Return the block + return block + + def _untag(self, tree): + for i, child in enumerate(tree): + if isinstance(child, Tree): + self._untag(child) + elif isinstance(child, tuple): + tree[i] = child[0] + else: + raise ValueError("expected child to be Tree or tuple") + return tree diff --git a/nltk/corpus/reader/cmudict.py b/nltk/corpus/reader/cmudict.py new file mode 100644 index 0000000..ca3de21 --- /dev/null +++ b/nltk/corpus/reader/cmudict.py @@ -0,0 +1,93 @@ +# Natural Language Toolkit: Carnegie Mellon Pronouncing Dictionary Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +The Carnegie Mellon Pronouncing Dictionary [cmudict.0.6] +ftp://ftp.cs.cmu.edu/project/speech/dict/ +Copyright 1998 Carnegie Mellon University + +File Format: Each line consists of an uppercased word, a counter +(for alternative pronunciations), and a transcription. Vowels are +marked for stress (1=primary, 2=secondary, 0=no stress). E.g.: +NATURAL 1 N AE1 CH ER0 AH0 L + +The dictionary contains 127069 entries. Of these, 119400 words are assigned +a unique pronunciation, 6830 words have two pronunciations, and 839 words have +three or more pronunciations. Many of these are fast-speech variants. + +Phonemes: There are 39 phonemes, as shown below: + +Phoneme Example Translation Phoneme Example Translation +------- ------- ----------- ------- ------- ----------- +AA odd AA D AE at AE T +AH hut HH AH T AO ought AO T +AW cow K AW AY hide HH AY D +B be B IY CH cheese CH IY Z +D dee D IY DH thee DH IY +EH Ed EH D ER hurt HH ER T +EY ate EY T F fee F IY +G green G R IY N HH he HH IY +IH it IH T IY eat IY T +JH gee JH IY K key K IY +L lee L IY M me M IY +N knee N IY NG ping P IH NG +OW oat OW T OY toy T OY +P pee P IY R read R IY D +S sea S IY SH she SH IY +T tea T IY TH theta TH EY T AH +UH hood HH UH D UW two T UW +V vee V IY W we W IY +Y yield Y IY L D Z zee Z IY +ZH seizure S IY ZH ER +""" + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.util import Index + + +class CMUDictCorpusReader(CorpusReader): + def entries(self): + """ + :return: the cmudict lexicon as a list of entries + containing (word, transcriptions) tuples. + """ + return concat( + [ + StreamBackedCorpusView(fileid, read_cmudict_block, encoding=enc) + for fileid, enc in self.abspaths(None, True) + ] + ) + + def words(self): + """ + :return: a list of all words defined in the cmudict lexicon. + """ + return [word.lower() for (word, _) in self.entries()] + + def dict(self): + """ + :return: the cmudict lexicon as a dictionary, whose keys are + lowercase words and whose values are lists of pronunciations. + """ + return dict(Index(self.entries())) + + +def read_cmudict_block(stream): + entries = [] + while len(entries) < 100: # Read 100 at a time. + line = stream.readline() + if line == "": + return entries # end of file. + pieces = line.split() + # A blank / whitespace-only line carries no entry; skipping it avoids an + # IndexError on ``pieces[0]`` that would otherwise abort iteration over + # the whole corpus. + if not pieces: + continue + entries.append((pieces[0].lower(), pieces[2:])) + return entries diff --git a/nltk/corpus/reader/comparative_sents.py b/nltk/corpus/reader/comparative_sents.py new file mode 100644 index 0000000..6077492 --- /dev/null +++ b/nltk/corpus/reader/comparative_sents.py @@ -0,0 +1,309 @@ +# Natural Language Toolkit: Comparative Sentence Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +CorpusReader for the Comparative Sentence Dataset. + +- Comparative Sentence Dataset information - + +Annotated by: Nitin Jindal and Bing Liu, 2006. + Department of Computer Sicence + University of Illinois at Chicago + +Contact: Nitin Jindal, njindal@cs.uic.edu + Bing Liu, liub@cs.uic.edu (https://www.cs.uic.edu/~liub) + +Distributed with permission. + +Related papers: + +- Nitin Jindal and Bing Liu. "Identifying Comparative Sentences in Text Documents". + Proceedings of the ACM SIGIR International Conference on Information Retrieval + (SIGIR-06), 2006. + +- Nitin Jindal and Bing Liu. "Mining Comprative Sentences and Relations". + Proceedings of Twenty First National Conference on Artificial Intelligence + (AAAI-2006), 2006. + +- Murthy Ganapathibhotla and Bing Liu. "Mining Opinions in Comparative Sentences". + Proceedings of the 22nd International Conference on Computational Linguistics + (Coling-2008), Manchester, 18-22 August, 2008. +""" +import re + +from nltk.corpus.reader.api import * +from nltk.tokenize import * + +# Regular expressions for dataset components +STARS = re.compile(r"^\*+$") +COMPARISON = re.compile(r"") +CLOSE_COMPARISON = re.compile(r"") +GRAD_COMPARISON = re.compile(r"") +NON_GRAD_COMPARISON = re.compile(r"") +ENTITIES_FEATS = re.compile(r"(\d)_((?:[\.\w\s/-](?!\d_))+)") +KEYWORD = re.compile(r"\(([^\(]*)\)$") + + +class Comparison: + """ + A Comparison represents a comparative sentence and its constituents. + """ + + def __init__( + self, + text=None, + comp_type=None, + entity_1=None, + entity_2=None, + feature=None, + keyword=None, + ): + """ + :param text: a string (optionally tokenized) containing a comparison. + :param comp_type: an integer defining the type of comparison expressed. + Values can be: 1 (Non-equal gradable), 2 (Equative), 3 (Superlative), + 4 (Non-gradable). + :param entity_1: the first entity considered in the comparison relation. + :param entity_2: the second entity considered in the comparison relation. + :param feature: the feature considered in the comparison relation. + :param keyword: the word or phrase which is used for that comparative relation. + """ + self.text = text + self.comp_type = comp_type + self.entity_1 = entity_1 + self.entity_2 = entity_2 + self.feature = feature + self.keyword = keyword + + def __repr__(self): + return ( + 'Comparison(text="{}", comp_type={}, entity_1="{}", entity_2="{}", ' + 'feature="{}", keyword="{}")' + ).format( + self.text, + self.comp_type, + self.entity_1, + self.entity_2, + self.feature, + self.keyword, + ) + + +class ComparativeSentencesCorpusReader(CorpusReader): + """ + Reader for the Comparative Sentence Dataset by Jindal and Liu (2006). + + >>> from nltk.corpus import comparative_sentences + >>> comparison = comparative_sentences.comparisons()[0] + >>> comparison.text # doctest: +NORMALIZE_WHITESPACE + ['its', 'fast-forward', 'and', 'rewind', 'work', 'much', 'more', 'smoothly', + 'and', 'consistently', 'than', 'those', 'of', 'other', 'models', 'i', "'ve", + 'had', '.'] + >>> comparison.entity_2 + 'models' + >>> (comparison.feature, comparison.keyword) + ('rewind', 'more') + >>> len(comparative_sentences.comparisons()) + 853 + """ + + CorpusView = StreamBackedCorpusView + + def __init__( + self, + root, + fileids, + word_tokenizer=WhitespaceTokenizer(), + sent_tokenizer=None, + encoding="utf8", + ): + """ + :param root: The root directory for this corpus. + :param fileids: a list or regexp specifying the fileids in this corpus. + :param word_tokenizer: tokenizer for breaking sentences or paragraphs + into words. Default: `WhitespaceTokenizer` + :param sent_tokenizer: tokenizer for breaking paragraphs into sentences. + :param encoding: the encoding that should be used to read the corpus. + """ + + CorpusReader.__init__(self, root, fileids, encoding) + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + self._readme = "README.txt" + + def comparisons(self, fileids=None): + """ + Return all comparisons in the corpus. + + :param fileids: a list or regexp specifying the ids of the files whose + comparisons have to be returned. + :return: the given file(s) as a list of Comparison objects. + :rtype: list(Comparison) + """ + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(path, self._read_comparison_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def keywords(self, fileids=None): + """ + Return a set of all keywords used in the corpus. + + :param fileids: a list or regexp specifying the ids of the files whose + keywords have to be returned. + :return: the set of keywords and comparative phrases used in the corpus. + :rtype: set(str) + """ + all_keywords = concat( + [ + self.CorpusView(path, self._read_keyword_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + keywords_set = {keyword.lower() for keyword in all_keywords if keyword} + return keywords_set + + def keywords_readme(self): + """ + Return the list of words and constituents considered as clues of a + comparison (from listOfkeywords.txt). + """ + keywords = [] + with self.open("listOfkeywords.txt") as fp: + raw_text = fp.read() + for line in raw_text.split("\n"): + if not line or line.startswith("//"): + continue + keywords.append(line.strip()) + return keywords + + def sents(self, fileids=None): + """ + Return all sentences in the corpus. + + :param fileids: a list or regexp specifying the ids of the files whose + sentences have to be returned. + :return: all sentences of the corpus as lists of tokens (or as plain + strings, if no word tokenizer is specified). + :rtype: list(list(str)) or list(str) + """ + return concat( + [ + self.CorpusView(path, self._read_sent_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def words(self, fileids=None): + """ + Return all words and punctuation symbols in the corpus. + + :param fileids: a list or regexp specifying the ids of the files whose + words have to be returned. + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + self.CorpusView(path, self._read_word_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def _read_comparison_block(self, stream): + while True: + line = stream.readline() + if not line: + return [] # end of file. + comparison_tags = re.findall(COMPARISON, line) + if comparison_tags: + grad_comparisons = re.findall(GRAD_COMPARISON, line) + non_grad_comparisons = re.findall(NON_GRAD_COMPARISON, line) + # Advance to the next line (it contains the comparative sentence) + comparison_text = stream.readline().strip() + if self._word_tokenizer: + comparison_text = self._word_tokenizer.tokenize(comparison_text) + # Skip the next line (it contains closing comparison tags) + stream.readline() + # If gradable comparisons are found, create Comparison instances + # and populate their fields + comparison_bundle = [] + if grad_comparisons: + # Each comparison tag has its own relations on a separate line + for comp in grad_comparisons: + comp_type = int(re.match(r"", comp).group(1)) + comparison = Comparison( + text=comparison_text, comp_type=comp_type + ) + line = stream.readline() + entities_feats = ENTITIES_FEATS.findall(line) + if entities_feats: + for code, entity_feat in entities_feats: + if code == "1": + comparison.entity_1 = entity_feat.strip() + elif code == "2": + comparison.entity_2 = entity_feat.strip() + elif code == "3": + comparison.feature = entity_feat.strip() + keyword = KEYWORD.findall(line) + if keyword: + comparison.keyword = keyword[0] + comparison_bundle.append(comparison) + # If non-gradable comparisons are found, create a simple Comparison + # instance for each one + if non_grad_comparisons: + for comp in non_grad_comparisons: + # comp_type in this case should always be 4. + comp_type = int(re.match(r"", comp).group(1)) + comparison = Comparison( + text=comparison_text, comp_type=comp_type + ) + comparison_bundle.append(comparison) + # Flatten the list of comparisons before returning them + # return concat([comparison_bundle]) + return comparison_bundle + + def _read_keyword_block(self, stream): + keywords = [] + for comparison in self._read_comparison_block(stream): + keywords.append(comparison.keyword) + return keywords + + def _read_sent_block(self, stream): + while True: + line = stream.readline() + if re.match(STARS, line): + while True: + line = stream.readline() + if re.match(STARS, line): + break + continue + if ( + not re.findall(COMPARISON, line) + and not ENTITIES_FEATS.findall(line) + and not re.findall(CLOSE_COMPARISON, line) + ): + if self._sent_tokenizer: + return [ + self._word_tokenizer.tokenize(sent) + for sent in self._sent_tokenizer.tokenize(line) + ] + else: + return [self._word_tokenizer.tokenize(line)] + + def _read_word_block(self, stream): + words = [] + for sent in self._read_sent_block(stream): + words.extend(sent) + return words diff --git a/nltk/corpus/reader/conll.py b/nltk/corpus/reader/conll.py new file mode 100644 index 0000000..2a13b18 --- /dev/null +++ b/nltk/corpus/reader/conll.py @@ -0,0 +1,608 @@ +# Natural Language Toolkit: CONLL Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Read CoNLL-style chunk fileids. +""" + +import textwrap + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tag import map_tag +from nltk.tree import Tree +from nltk.util import LazyConcatenation, LazyMap + + +class ConllCorpusReader(CorpusReader): + """ + A corpus reader for CoNLL-style files. These files consist of a + series of sentences, separated by blank lines. Each sentence is + encoded using a table (or "grid") of values, where each line + corresponds to a single word, and each column corresponds to an + annotation type. The set of columns used by CoNLL-style files can + vary from corpus to corpus; the ``ConllCorpusReader`` constructor + therefore takes an argument, ``columntypes``, which is used to + specify the columns that are used by a given corpus. By default + columns are split by consecutive whitespaces, with the + ``separator`` argument you can set a string to split by (e.g. + ``\'\t\'``). + + + @todo: Add support for reading from corpora where different + parallel files contain different columns. + @todo: Possibly add caching of the grid corpus view? This would + allow the same grid view to be used by different data access + methods (eg words() and parsed_sents() could both share the + same grid corpus view object). + @todo: Better support for -DOCSTART-. Currently, we just ignore + it, but it could be used to define methods that retrieve a + document at a time (eg parsed_documents()). + """ + + # ///////////////////////////////////////////////////////////////// + # Column Types + # ///////////////////////////////////////////////////////////////// + + WORDS = "words" #: column type for words + POS = "pos" #: column type for part-of-speech tags + TREE = "tree" #: column type for parse trees + CHUNK = "chunk" #: column type for chunk structures + NE = "ne" #: column type for named entities + SRL = "srl" #: column type for semantic role labels + IGNORE = "ignore" #: column type for column that should be ignored + + #: A list of all column types supported by the conll corpus reader. + COLUMN_TYPES = (WORDS, POS, TREE, CHUNK, NE, SRL, IGNORE) + + # ///////////////////////////////////////////////////////////////// + # Constructor + # ///////////////////////////////////////////////////////////////// + + def __init__( + self, + root, + fileids, + columntypes, + chunk_types=None, + root_label="S", + pos_in_tree=False, + srl_includes_roleset=True, + encoding="utf8", + tree_class=Tree, + tagset=None, + separator=None, + ): + for columntype in columntypes: + if columntype not in self.COLUMN_TYPES: + raise ValueError("Bad column type %r" % columntype) + if isinstance(chunk_types, str): + chunk_types = [chunk_types] + self._chunk_types = chunk_types + self._colmap = {c: i for (i, c) in enumerate(columntypes)} + self._pos_in_tree = pos_in_tree + self._root_label = root_label # for chunks + self._srl_includes_roleset = srl_includes_roleset + self._tree_class = tree_class + CorpusReader.__init__(self, root, fileids, encoding) + self._tagset = tagset + self.sep = separator + + # ///////////////////////////////////////////////////////////////// + # Data Access Methods + # ///////////////////////////////////////////////////////////////// + + def words(self, fileids=None): + self._require(self.WORDS) + return LazyConcatenation(LazyMap(self._get_words, self._grids(fileids))) + + def sents(self, fileids=None): + self._require(self.WORDS) + return LazyMap(self._get_words, self._grids(fileids)) + + def tagged_words(self, fileids=None, tagset=None): + self._require(self.WORDS, self.POS) + + def get_tagged_words(grid): + return self._get_tagged_words(grid, tagset) + + return LazyConcatenation(LazyMap(get_tagged_words, self._grids(fileids))) + + def tagged_sents(self, fileids=None, tagset=None): + self._require(self.WORDS, self.POS) + + def get_tagged_words(grid): + return self._get_tagged_words(grid, tagset) + + return LazyMap(get_tagged_words, self._grids(fileids)) + + def chunked_words(self, fileids=None, chunk_types=None, tagset=None): + self._require(self.WORDS, self.POS, self.CHUNK) + if chunk_types is None: + chunk_types = self._chunk_types + + def get_chunked_words(grid): # capture chunk_types as local var + return self._get_chunked_words(grid, chunk_types, tagset) + + return LazyConcatenation(LazyMap(get_chunked_words, self._grids(fileids))) + + def chunked_sents(self, fileids=None, chunk_types=None, tagset=None): + self._require(self.WORDS, self.POS, self.CHUNK) + if chunk_types is None: + chunk_types = self._chunk_types + + def get_chunked_words(grid): # capture chunk_types as local var + return self._get_chunked_words(grid, chunk_types, tagset) + + return LazyMap(get_chunked_words, self._grids(fileids)) + + def parsed_sents(self, fileids=None, pos_in_tree=None, tagset=None): + self._require(self.WORDS, self.POS, self.TREE) + if pos_in_tree is None: + pos_in_tree = self._pos_in_tree + + def get_parsed_sent(grid): # capture pos_in_tree as local var + return self._get_parsed_sent(grid, pos_in_tree, tagset) + + return LazyMap(get_parsed_sent, self._grids(fileids)) + + def srl_spans(self, fileids=None): + self._require(self.SRL) + return LazyMap(self._get_srl_spans, self._grids(fileids)) + + def srl_instances(self, fileids=None, pos_in_tree=None, flatten=True): + self._require(self.WORDS, self.POS, self.TREE, self.SRL) + if pos_in_tree is None: + pos_in_tree = self._pos_in_tree + + def get_srl_instances(grid): # capture pos_in_tree as local var + return self._get_srl_instances(grid, pos_in_tree) + + result = LazyMap(get_srl_instances, self._grids(fileids)) + if flatten: + result = LazyConcatenation(result) + return result + + def iob_words(self, fileids=None, tagset=None): + """ + :return: a list of word/tag/IOB tuples + :rtype: list(tuple) + :param fileids: the list of fileids that make up this corpus + :type fileids: None or str or list + """ + self._require(self.WORDS, self.POS, self.CHUNK) + + def get_iob_words(grid): + return self._get_iob_words(grid, tagset) + + return LazyConcatenation(LazyMap(get_iob_words, self._grids(fileids))) + + def iob_sents(self, fileids=None, tagset=None): + """ + :return: a list of lists of word/tag/IOB tuples + :rtype: list(list) + :param fileids: the list of fileids that make up this corpus + :type fileids: None or str or list + """ + self._require(self.WORDS, self.POS, self.CHUNK) + + def get_iob_words(grid): + return self._get_iob_words(grid, tagset) + + return LazyMap(get_iob_words, self._grids(fileids)) + + # ///////////////////////////////////////////////////////////////// + # Grid Reading + # ///////////////////////////////////////////////////////////////// + + def _grids(self, fileids=None): + # n.b.: we could cache the object returned here (keyed on + # fileids), which would let us reuse the same corpus view for + # different things (eg srl and parse trees). + return concat( + [ + StreamBackedCorpusView(fileid, self._read_grid_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def _read_grid_block(self, stream): + grids = [] + for block in read_blankline_block(stream): + block = block.strip() + if not block: + continue + + grid = [line.split(self.sep) for line in block.split("\n")] + + # If there's a docstart row, then discard. ([xx] eventually it + # would be good to actually use it) + if grid[0][self._colmap.get("words", 0)] == "-DOCSTART-": + del grid[0] + + # Check that the grid is consistent. + for row in grid: + if len(row) != len(grid[0]): + raise ValueError("Inconsistent number of columns:\n%s" % block) + grids.append(grid) + return grids + + # ///////////////////////////////////////////////////////////////// + # Transforms + # ///////////////////////////////////////////////////////////////// + # given a grid, transform it into some representation (e.g., + # a list of words or a parse tree). + + def _get_words(self, grid): + return self._get_column(grid, self._colmap["words"]) + + def _get_tagged_words(self, grid, tagset=None): + pos_tags = self._get_column(grid, self._colmap["pos"]) + if tagset and tagset != self._tagset: + pos_tags = [map_tag(self._tagset, tagset, t) for t in pos_tags] + return list(zip(self._get_column(grid, self._colmap["words"]), pos_tags)) + + def _get_iob_words(self, grid, tagset=None): + pos_tags = self._get_column(grid, self._colmap["pos"]) + if tagset and tagset != self._tagset: + pos_tags = [map_tag(self._tagset, tagset, t) for t in pos_tags] + return list( + zip( + self._get_column(grid, self._colmap["words"]), + pos_tags, + self._get_column(grid, self._colmap["chunk"]), + ) + ) + + def _get_chunked_words(self, grid, chunk_types, tagset=None): + # n.b.: this method is very similar to conllstr2tree. + words = self._get_column(grid, self._colmap["words"]) + pos_tags = self._get_column(grid, self._colmap["pos"]) + if tagset and tagset != self._tagset: + pos_tags = [map_tag(self._tagset, tagset, t) for t in pos_tags] + chunk_tags = self._get_column(grid, self._colmap["chunk"]) + + stack = [Tree(self._root_label, [])] + + for word, pos_tag, chunk_tag in zip(words, pos_tags, chunk_tags): + if chunk_tag == "O": + state, chunk_type = "O", "" + else: + # A tag that is neither "O" nor a well-formed "-" + # would otherwise raise a cryptic "not enough values to unpack" + # error, or be silently mishandled, and abort iteration over the + # whole corpus; validate the IOB shape and fail with a clear, + # catchable message instead. ``split("-", 1)`` keeps chunk types + # that legitimately contain a hyphen (e.g. "B-NP-SBJ"). + parts = chunk_tag.split("-", 1) + state = parts[0] + chunk_type = parts[1] if len(parts) == 2 else "" + if state not in ("B", "I") or not chunk_type: + raise ValueError( + f"Malformed chunk tag {chunk_tag!r}: expected 'O' or " + "'-' (e.g. 'B-NP')" + ) + # If it's a chunk we don't care about, treat it as O. + if chunk_types is not None and chunk_type not in chunk_types: + state = "O" + # Treat a mismatching I like a B. + if state == "I" and chunk_type != stack[-1].label(): + state = "B" + # For B or I: close any open chunks + if state in "BO" and len(stack) == 2: + stack.pop() + # For B: start a new chunk. + if state == "B": + new_chunk = Tree(chunk_type, []) + stack[-1].append(new_chunk) + stack.append(new_chunk) + # Add the word token. + stack[-1].append((word, pos_tag)) + + return stack[0] + + def _get_parsed_sent(self, grid, pos_in_tree, tagset=None): + words = self._get_column(grid, self._colmap["words"]) + pos_tags = self._get_column(grid, self._colmap["pos"]) + if tagset and tagset != self._tagset: + pos_tags = [map_tag(self._tagset, tagset, t) for t in pos_tags] + parse_tags = self._get_column(grid, self._colmap["tree"]) + + treestr = "" + for word, pos_tag, parse_tag in zip(words, pos_tags, parse_tags): + if word == "(": + word = "-LRB-" + if word == ")": + word = "-RRB-" + if pos_tag == "(": + pos_tag = "-LRB-" + if pos_tag == ")": + pos_tag = "-RRB-" + # A parse tag missing its '*' word placeholder would otherwise raise + # a cryptic unpacking error that aborts the whole-corpus iteration. + parts = parse_tag.split("*") + if len(parts) != 2: + raise ValueError( + f"Malformed parse tag {parse_tag!r}: expected exactly one " + "'*' word placeholder (e.g. '(NP*' or '*)')" + ) + (left, right) = parts + right = right.count(")") * ")" # only keep ')'. + treestr += f"{left} ({pos_tag} {word}) {right}" + try: + tree = self._tree_class.fromstring(treestr) + except (ValueError, IndexError): + tree = self._tree_class.fromstring(f"({self._root_label} {treestr})") + + if not pos_in_tree: + for subtree in tree.subtrees(): + for i, child in enumerate(subtree): + if ( + isinstance(child, Tree) + and len(child) == 1 + and isinstance(child[0], str) + ): + subtree[i] = (child[0], child.label()) + + return tree + + def _get_srl_spans(self, grid): + """ + list of list of (start, end), tag) tuples + """ + if self._srl_includes_roleset: + predicates = self._get_column(grid, self._colmap["srl"] + 1) + start_col = self._colmap["srl"] + 2 + else: + predicates = self._get_column(grid, self._colmap["srl"]) + start_col = self._colmap["srl"] + 1 + + # Count how many predicates there are. This tells us how many + # columns to expect for SRL data. + num_preds = len([p for p in predicates if p != "-"]) + + spanlists = [] + for i in range(num_preds): + col = self._get_column(grid, start_col + i) + spanlist = [] + stack = [] + for wordnum, srl_tag in enumerate(col): + # A SRL tag missing its '*' word placeholder would otherwise + # raise a cryptic unpacking error that aborts the whole corpus. + parts = srl_tag.split("*") + if len(parts) != 2: + raise ValueError( + f"Malformed SRL tag {srl_tag!r}: expected exactly one " + "'*' word placeholder (e.g. '(A0*' or '*)')" + ) + (left, right) = parts + for tag in left.split("("): + if tag: + stack.append((tag, wordnum)) + for i in range(right.count(")")): + (tag, start) = stack.pop() + spanlist.append(((start, wordnum + 1), tag)) + spanlists.append(spanlist) + + return spanlists + + def _get_srl_instances(self, grid, pos_in_tree): + tree = self._get_parsed_sent(grid, pos_in_tree) + spanlists = self._get_srl_spans(grid) + if self._srl_includes_roleset: + predicates = self._get_column(grid, self._colmap["srl"] + 1) + rolesets = self._get_column(grid, self._colmap["srl"]) + else: + predicates = self._get_column(grid, self._colmap["srl"]) + rolesets = [None] * len(predicates) + + instances = ConllSRLInstanceList(tree) + for wordnum, predicate in enumerate(predicates): + if predicate == "-": + continue + # Decide which spanlist to use. Don't assume that they're + # sorted in the same order as the predicates (even though + # they usually are). + for spanlist in spanlists: + for (start, end), tag in spanlist: + if wordnum in range(start, end) and tag in ("V", "C-V"): + break + else: + continue + break + else: + raise ValueError("No srl column found for %r" % predicate) + instances.append( + ConllSRLInstance(tree, wordnum, predicate, rolesets[wordnum], spanlist) + ) + + return instances + + # ///////////////////////////////////////////////////////////////// + # Helper Methods + # ///////////////////////////////////////////////////////////////// + + def _require(self, *columntypes): + for columntype in columntypes: + if columntype not in self._colmap: + raise ValueError( + "This corpus does not contain a %s " "column." % columntype + ) + + @staticmethod + def _get_column(grid, column_index): + return [grid[i][column_index] for i in range(len(grid))] + + +class ConllSRLInstance: + """ + An SRL instance from a CoNLL corpus, which identifies and + providing labels for the arguments of a single verb. + """ + + # [xx] add inst.core_arguments, inst.argm_arguments? + + def __init__(self, tree, verb_head, verb_stem, roleset, tagged_spans): + self.verb = [] + """A list of the word indices of the words that compose the + verb whose arguments are identified by this instance. + This will contain multiple word indices when multi-word + verbs are used (e.g. 'turn on').""" + + self.verb_head = verb_head + """The word index of the head word of the verb whose arguments + are identified by this instance. E.g., for a sentence that + uses the verb 'turn on,' ``verb_head`` will be the word index + of the word 'turn'.""" + + self.verb_stem = verb_stem + + self.roleset = roleset + + self.arguments = [] + """A list of ``(argspan, argid)`` tuples, specifying the location + and type for each of the arguments identified by this + instance. ``argspan`` is a tuple ``start, end``, indicating + that the argument consists of the ``words[start:end]``.""" + + self.tagged_spans = tagged_spans + """A list of ``(span, id)`` tuples, specifying the location and + type for each of the arguments, as well as the verb pieces, + that make up this instance.""" + + self.tree = tree + """The parse tree for the sentence containing this instance.""" + + self.words = tree.leaves() + """A list of the words in the sentence containing this + instance.""" + + # Fill in the self.verb and self.arguments values. + for (start, end), tag in tagged_spans: + if tag in ("V", "C-V"): + self.verb += list(range(start, end)) + else: + self.arguments.append(((start, end), tag)) + + def __repr__(self): + # Originally, its: + ##plural = 's' if len(self.arguments) != 1 else '' + plural = "s" if len(self.arguments) != 1 else "" + return "" % ( + (self.verb_stem, len(self.arguments), plural) + ) + + def pprint(self): + verbstr = " ".join(self.words[i][0] for i in self.verb) + hdr = f"SRL for {verbstr!r} (stem={self.verb_stem!r}):\n" + s = "" + for i, word in enumerate(self.words): + if isinstance(word, tuple): + word = word[0] + for (start, end), argid in self.arguments: + if i == start: + s += "[%s " % argid + if i == end: + s += "] " + if i in self.verb: + word = "<<%s>>" % word + s += word + " " + return hdr + textwrap.fill( + s.replace(" ]", "]"), initial_indent=" ", subsequent_indent=" " + ) + + +class ConllSRLInstanceList(list): + """ + Set of instances for a single sentence + """ + + def __init__(self, tree, instances=()): + self.tree = tree + list.__init__(self, instances) + + def __str__(self): + return self.pprint() + + def pprint(self, include_tree=False): + # Sanity check: trees should be the same + for inst in self: + if inst.tree != self.tree: + raise ValueError("Tree mismatch!") + + # If desired, add trees: + if include_tree: + words = self.tree.leaves() + pos = [None] * len(words) + synt = ["*"] * len(words) + self._tree2conll(self.tree, 0, words, pos, synt) + + s = "" + for i in range(len(words)): + # optional tree columns + if include_tree: + s += "%-20s " % words[i] + s += "%-8s " % pos[i] + s += "%15s*%-8s " % tuple(synt[i].split("*")) + + # verb head column + for inst in self: + if i == inst.verb_head: + s += "%-20s " % inst.verb_stem + break + else: + s += "%-20s " % "-" + # Remaining columns: self + for inst in self: + argstr = "*" + for (start, end), argid in inst.tagged_spans: + if i == start: + argstr = f"({argid}{argstr}" + if i == (end - 1): + argstr += ")" + s += "%-12s " % argstr + s += "\n" + return s + + def _tree2conll(self, tree, wordnum, words, pos, synt): + assert isinstance(tree, Tree) + if len(tree) == 1 and isinstance(tree[0], str): + pos[wordnum] = tree.label() + assert words[wordnum] == tree[0] + return wordnum + 1 + elif len(tree) == 1 and isinstance(tree[0], tuple): + assert len(tree[0]) == 2 + pos[wordnum], pos[wordnum] = tree[0] + return wordnum + 1 + else: + synt[wordnum] = f"({tree.label()}{synt[wordnum]}" + for child in tree: + wordnum = self._tree2conll(child, wordnum, words, pos, synt) + synt[wordnum - 1] += ")" + return wordnum + + +class ConllChunkCorpusReader(ConllCorpusReader): + """ + A ConllCorpusReader whose data file contains three columns: words, + pos, and chunk. + """ + + def __init__( + self, root, fileids, chunk_types, encoding="utf8", tagset=None, separator=None + ): + ConllCorpusReader.__init__( + self, + root, + fileids, + ("words", "pos", "chunk"), + chunk_types=chunk_types, + encoding=encoding, + tagset=tagset, + separator=separator, + ) diff --git a/nltk/corpus/reader/crubadan.py b/nltk/corpus/reader/crubadan.py new file mode 100644 index 0000000..5fc30e1 --- /dev/null +++ b/nltk/corpus/reader/crubadan.py @@ -0,0 +1,106 @@ +# Natural Language Toolkit: An Crubadan N-grams Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Avital Pekker +# +# URL: +# For license information, see LICENSE.TXT + +""" +An NLTK interface for the n-gram statistics gathered from +the corpora for each language using An Crubadan. + +There are multiple potential applications for the data but +this reader was created with the goal of using it in the +context of language identification. + +For details about An Crubadan, this data, and its potential uses, see: +http://borel.slu.edu/crubadan/index.html +""" + +import re +from os import path + +from nltk.corpus.reader import CorpusReader +from nltk.data import ZipFilePathPointer +from nltk.probability import FreqDist + + +class CrubadanCorpusReader(CorpusReader): + """ + A corpus reader used to access language An Crubadan n-gram files. + """ + + _LANG_MAPPER_FILE = "table.txt" + _all_lang_freq = {} + + def __init__(self, root, fileids, encoding="utf8", tagset=None): + super().__init__(root, fileids, encoding="utf8") + self._lang_mapping_data = [] + self._load_lang_mapping_data() + + def lang_freq(self, lang): + """Return n-gram FreqDist for a specific language + given ISO 639-3 language code""" + + if lang not in self._all_lang_freq: + self._all_lang_freq[lang] = self._load_lang_ngrams(lang) + + return self._all_lang_freq[lang] + + def langs(self): + """Return a list of supported languages as ISO 639-3 codes""" + return [row[1] for row in self._lang_mapping_data] + + def iso_to_crubadan(self, lang): + """Return internal Crubadan code based on ISO 639-3 code""" + for i in self._lang_mapping_data: + if i[1].lower() == lang.lower(): + return i[0] + + def crubadan_to_iso(self, lang): + """Return ISO 639-3 code given internal Crubadan code""" + for i in self._lang_mapping_data: + if i[0].lower() == lang.lower(): + return i[1] + + def _load_lang_mapping_data(self): + """Load language mappings between codes and description from table.txt""" + if isinstance(self.root, ZipFilePathPointer): + raise RuntimeError( + "Please install the 'crubadan' corpus first, use nltk.download()" + ) + + mapper_file = path.join(self.root, self._LANG_MAPPER_FILE) + if self._LANG_MAPPER_FILE not in self.fileids(): + raise RuntimeError("Could not find language mapper file: " + mapper_file) + + with open(mapper_file, encoding="utf-8") as raw: + strip_raw = raw.read().strip() + + self._lang_mapping_data = [row.split("\t") for row in strip_raw.split("\n")] + + def _load_lang_ngrams(self, lang): + """Load single n-gram language file given the ISO 639-3 language code + and return its FreqDist""" + + if lang not in self.langs(): + raise RuntimeError("Unsupported language.") + + crubadan_code = self.iso_to_crubadan(lang) + ngram_file = path.join(self.root, crubadan_code + "-3grams.txt") + + if not path.isfile(ngram_file): + raise RuntimeError("No N-gram file found for requested language.") + + counts = FreqDist() + with open(ngram_file, encoding="utf-8") as f: + for line in f: + data = line.split(" ") + + ngram = data[1].strip("\n") + freq = int(data[0]) + + counts[ngram] = freq + + return counts diff --git a/nltk/corpus/reader/dependency.py b/nltk/corpus/reader/dependency.py new file mode 100644 index 0000000..3790d9b --- /dev/null +++ b/nltk/corpus/reader/dependency.py @@ -0,0 +1,115 @@ +# Natural Language Toolkit: Dependency Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Kepa Sarasola +# Iker Manterola +# +# URL: +# For license information, see LICENSE.TXT + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.parse import DependencyGraph +from nltk.tokenize import * + + +class DependencyCorpusReader(SyntaxCorpusReader): + def __init__( + self, + root, + fileids, + encoding="utf8", + word_tokenizer=TabTokenizer(), + sent_tokenizer=RegexpTokenizer("\n", gaps=True), + para_block_reader=read_blankline_block, + ): + SyntaxCorpusReader.__init__(self, root, fileids, encoding) + + ######################################################### + + def words(self, fileids=None): + return concat( + [ + DependencyCorpusView(fileid, False, False, False, encoding=enc) + for fileid, enc in self.abspaths(fileids, include_encoding=True) + ] + ) + + def tagged_words(self, fileids=None): + return concat( + [ + DependencyCorpusView(fileid, True, False, False, encoding=enc) + for fileid, enc in self.abspaths(fileids, include_encoding=True) + ] + ) + + def sents(self, fileids=None): + return concat( + [ + DependencyCorpusView(fileid, False, True, False, encoding=enc) + for fileid, enc in self.abspaths(fileids, include_encoding=True) + ] + ) + + def tagged_sents(self, fileids=None): + return concat( + [ + DependencyCorpusView(fileid, True, True, False, encoding=enc) + for fileid, enc in self.abspaths(fileids, include_encoding=True) + ] + ) + + def parsed_sents(self, fileids=None): + sents = concat( + [ + DependencyCorpusView(fileid, False, True, True, encoding=enc) + for fileid, enc in self.abspaths(fileids, include_encoding=True) + ] + ) + return [DependencyGraph(sent) for sent in sents] + + +class DependencyCorpusView(StreamBackedCorpusView): + _DOCSTART = "-DOCSTART- -DOCSTART- O\n" # dokumentu hasiera definitzen da + + def __init__( + self, + corpus_file, + tagged, + group_by_sent, + dependencies, + chunk_types=None, + encoding="utf8", + ): + self._tagged = tagged + self._dependencies = dependencies + self._group_by_sent = group_by_sent + self._chunk_types = chunk_types + StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) + + def read_block(self, stream): + # Read the next sentence. + sent = read_blankline_block(stream)[0].strip() + # Strip off the docstart marker, if present. + if sent.startswith(self._DOCSTART): + sent = sent[len(self._DOCSTART) :].lstrip() + + # extract word and tag from any of the formats + if not self._dependencies: + lines = [line.split("\t") for line in sent.split("\n")] + if len(lines[0]) == 3 or len(lines[0]) == 4: + sent = [(line[0], line[1]) for line in lines] + elif len(lines[0]) == 10: + sent = [(line[1], line[4]) for line in lines] + else: + raise ValueError("Unexpected number of fields in dependency tree file") + + # discard tags if they weren't requested + if not self._tagged: + sent = [word for (word, tag) in sent] + + # Return the result. + if self._group_by_sent: + return [sent] + else: + return list(sent) diff --git a/nltk/corpus/reader/framenet.py b/nltk/corpus/reader/framenet.py new file mode 100644 index 0000000..35e0b6c --- /dev/null +++ b/nltk/corpus/reader/framenet.py @@ -0,0 +1,3469 @@ +# Natural Language Toolkit: Framenet Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Chuck Wooters , +# Nathan Schneider +# URL: +# For license information, see LICENSE.TXT + + +""" +Corpus reader for the FrameNet 1.7 lexicon and corpus. +""" + +import itertools +import ntpath +import os +import re +import sys +import textwrap +import types +from collections import OrderedDict, defaultdict +from itertools import zip_longest +from operator import itemgetter +from pprint import pprint + +from nltk.corpus.reader import XMLCorpusReader, XMLCorpusView +from nltk.util import LazyConcatenation, LazyIteratorList, LazyMap + +__docformat__ = "epytext en" + + +def mimic_wrap(lines, wrap_at=65, **kwargs): + """ + Wrap the first of 'lines' with textwrap and the remaining lines at exactly the same + positions as the first. + """ + l0 = textwrap.fill(lines[0], wrap_at, drop_whitespace=False).split("\n") + yield l0 + + def _(line): + il0 = 0 + while line and il0 < len(l0) - 1: + yield line[: len(l0[il0])] + line = line[len(l0[il0]) :] + il0 += 1 + if line: # Remaining stuff on this line past the end of the mimicked line. + # So just textwrap this line. + yield from textwrap.fill(line, wrap_at, drop_whitespace=False).split("\n") + + for l in lines[1:]: + yield list(_(l)) + + +def _pretty_longstring(defstr, prefix="", wrap_at=65): + """ + Helper function for pretty-printing a long string. + + :param defstr: The string to be printed. + :type defstr: str + :return: A nicely formatted string representation of the long string. + :rtype: str + """ + return "\n".join( + [prefix + line for line in textwrap.fill(defstr, wrap_at).split("\n")] + ) + + +def _pretty_any(obj): + """ + Helper function for pretty-printing any AttrDict object. + + :param obj: The obj to be printed. + :type obj: AttrDict + :return: A nicely formatted string representation of the AttrDict object. + :rtype: str + """ + + outstr = "" + for k in obj: + if isinstance(obj[k], str) and len(obj[k]) > 65: + outstr += f"[{k}]\n" + outstr += "{}".format(_pretty_longstring(obj[k], prefix=" ")) + outstr += "\n" + else: + outstr += f"[{k}] {obj[k]}\n" + + return outstr + + +def _pretty_semtype(st): + """ + Helper function for pretty-printing a semantic type. + + :param st: The semantic type to be printed. + :type st: AttrDict + :return: A nicely formatted string representation of the semantic type. + :rtype: str + """ + + semkeys = st.keys() + if len(semkeys) == 1: + return "" + + outstr = "" + outstr += "semantic type ({0.ID}): {0.name}\n".format(st) + if "abbrev" in semkeys: + outstr += f"[abbrev] {st.abbrev}\n" + if "definition" in semkeys: + outstr += "[definition]\n" + outstr += _pretty_longstring(st.definition, " ") + outstr += f"[rootType] {st.rootType.name}({st.rootType.ID})\n" + if st.superType is None: + outstr += "[superType] \n" + else: + outstr += f"[superType] {st.superType.name}({st.superType.ID})\n" + outstr += f"[subTypes] {len(st.subTypes)} subtypes\n" + outstr += ( + " " + + ", ".join(f"{x.name}({x.ID})" for x in st.subTypes) + + "\n" * (len(st.subTypes) > 0) + ) + return outstr + + +def _pretty_frame_relation_type(freltyp): + """ + Helper function for pretty-printing a frame relation type. + + :param freltyp: The frame relation type to be printed. + :type freltyp: AttrDict + :return: A nicely formatted string representation of the frame relation type. + :rtype: str + """ + outstr = " {0.subFrameName}>".format( + freltyp + ) + return outstr + + +def _pretty_frame_relation(frel): + """ + Helper function for pretty-printing a frame relation. + + :param frel: The frame relation to be printed. + :type frel: AttrDict + :return: A nicely formatted string representation of the frame relation. + :rtype: str + """ + outstr = "<{0.type.superFrameName}={0.superFrameName} -- {0.type.name} -> {0.type.subFrameName}={0.subFrameName}>".format( + frel + ) + return outstr + + +def _pretty_fe_relation(ferel): + """ + Helper function for pretty-printing an FE relation. + + :param ferel: The FE relation to be printed. + :type ferel: AttrDict + :return: A nicely formatted string representation of the FE relation. + :rtype: str + """ + outstr = "<{0.type.superFrameName}={0.frameRelation.superFrameName}.{0.superFEName} -- {0.type.name} -> {0.type.subFrameName}={0.frameRelation.subFrameName}.{0.subFEName}>".format( + ferel + ) + return outstr + + +def _pretty_lu(lu): + """ + Helper function for pretty-printing a lexical unit. + + :param lu: The lu to be printed. + :type lu: AttrDict + :return: A nicely formatted string representation of the lexical unit. + :rtype: str + """ + + lukeys = lu.keys() + outstr = "" + outstr += "lexical unit ({0.ID}): {0.name}\n\n".format(lu) + if "definition" in lukeys: + outstr += "[definition]\n" + outstr += _pretty_longstring(lu.definition, " ") + if "frame" in lukeys: + outstr += f"\n[frame] {lu.frame.name}({lu.frame.ID})\n" + if "incorporatedFE" in lukeys: + outstr += f"\n[incorporatedFE] {lu.incorporatedFE}\n" + if "POS" in lukeys: + outstr += f"\n[POS] {lu.POS}\n" + if "status" in lukeys: + outstr += f"\n[status] {lu.status}\n" + if "totalAnnotated" in lukeys: + outstr += f"\n[totalAnnotated] {lu.totalAnnotated} annotated examples\n" + if "lexemes" in lukeys: + outstr += "\n[lexemes] {}\n".format( + " ".join(f"{lex.name}/{lex.POS}" for lex in lu.lexemes) + ) + if "semTypes" in lukeys: + outstr += f"\n[semTypes] {len(lu.semTypes)} semantic types\n" + outstr += ( + " " * (len(lu.semTypes) > 0) + + ", ".join(f"{x.name}({x.ID})" for x in lu.semTypes) + + "\n" * (len(lu.semTypes) > 0) + ) + if "URL" in lukeys: + outstr += f"\n[URL] {lu.URL}\n" + if "subCorpus" in lukeys: + subc = [x.name for x in lu.subCorpus] + outstr += f"\n[subCorpus] {len(lu.subCorpus)} subcorpora\n" + for line in textwrap.fill(", ".join(sorted(subc)), 60).split("\n"): + outstr += f" {line}\n" + if "exemplars" in lukeys: + outstr += "\n[exemplars] {} sentences across all subcorpora\n".format( + len(lu.exemplars) + ) + + return outstr + + +def _pretty_exemplars(exemplars, lu): + """ + Helper function for pretty-printing a list of exemplar sentences for a lexical unit. + + :param sent: The list of exemplar sentences to be printed. + :type sent: list(AttrDict) + :return: An index of the text of the exemplar sentences. + :rtype: str + """ + + outstr = "" + outstr += "exemplar sentences for {0.name} in {0.frame.name}:\n\n".format(lu) + for i, sent in enumerate(exemplars): + outstr += f"[{i}] {sent.text}\n" + outstr += "\n" + return outstr + + +def _pretty_fulltext_sentences(sents): + """ + Helper function for pretty-printing a list of annotated sentences for a full-text document. + + :param sent: The list of sentences to be printed. + :type sent: list(AttrDict) + :return: An index of the text of the sentences. + :rtype: str + """ + + outstr = "" + outstr += "full-text document ({0.ID}) {0.name}:\n\n".format(sents) + outstr += "[corpid] {0.corpid}\n[corpname] {0.corpname}\n[description] {0.description}\n[URL] {0.URL}\n\n".format( + sents + ) + outstr += "[sentence]\n" + for i, sent in enumerate(sents.sentence): + outstr += f"[{i}] {sent.text}\n" + outstr += "\n" + return outstr + + +def _pretty_fulltext_sentence(sent): + """ + Helper function for pretty-printing an annotated sentence from a full-text document. + + :param sent: The sentence to be printed. + :type sent: list(AttrDict) + :return: The text of the sentence with annotation set indices on frame targets. + :rtype: str + """ + + outstr = "" + outstr += "full-text sentence ({0.ID}) in {1}:\n\n".format( + sent, sent.doc.get("name", sent.doc.description) + ) + outstr += f"\n[POS] {len(sent.POS)} tags\n" + outstr += f"\n[POS_tagset] {sent.POS_tagset}\n\n" + outstr += "[text] + [annotationSet]\n\n" + outstr += sent._ascii() # -> _annotation_ascii() + outstr += "\n" + return outstr + + +def _pretty_pos(aset): + """ + Helper function for pretty-printing a sentence with its POS tags. + + :param aset: The POS annotation set of the sentence to be printed. + :type sent: list(AttrDict) + :return: The text of the sentence and its POS tags. + :rtype: str + """ + + outstr = "" + outstr += "POS annotation set ({0.ID}) {0.POS_tagset} in sentence {0.sent.ID}:\n\n".format( + aset + ) + + # list the target spans and their associated aset index + overt = sorted(aset.POS) + + sent = aset.sent + s0 = sent.text + s1 = "" + s2 = "" + i = 0 + adjust = 0 + for j, k, lbl in overt: + assert j >= i, ("Overlapping targets?", (j, k, lbl)) + s1 += " " * (j - i) + "-" * (k - j) + if len(lbl) > (k - j): + # add space in the sentence to make room for the annotation index + amt = len(lbl) - (k - j) + s0 = ( + s0[: k + adjust] + "~" * amt + s0[k + adjust :] + ) # '~' to prevent line wrapping + s1 = s1[: k + adjust] + " " * amt + s1[k + adjust :] + adjust += amt + s2 += " " * (j - i) + lbl.ljust(k - j) + i = k + + long_lines = [s0, s1, s2] + + outstr += "\n\n".join( + map("\n".join, zip_longest(*mimic_wrap(long_lines), fillvalue=" ")) + ).replace("~", " ") + outstr += "\n" + return outstr + + +def _pretty_annotation(sent, aset_level=False): + """ + Helper function for pretty-printing an exemplar sentence for a lexical unit. + + :param sent: An annotation set or exemplar sentence to be printed. + :param aset_level: If True, 'sent' is actually an annotation set within a sentence. + :type sent: AttrDict + :return: A nicely formatted string representation of the exemplar sentence + with its target, frame, and FE annotations. + :rtype: str + """ + + sentkeys = sent.keys() + outstr = "annotation set" if aset_level else "exemplar sentence" + outstr += f" ({sent.ID}):\n" + if aset_level: # TODO: any UNANN exemplars? + outstr += f"\n[status] {sent.status}\n" + for k in ("corpID", "docID", "paragNo", "sentNo", "aPos"): + if k in sentkeys: + outstr += f"[{k}] {sent[k]}\n" + outstr += ( + "\n[LU] ({0.ID}) {0.name} in {0.frame.name}\n".format(sent.LU) + if sent.LU + else "\n[LU] Not found!" + ) + outstr += "\n[frame] ({0.ID}) {0.name}\n".format( + sent.frame + ) # redundant with above, but .frame is convenient + if not aset_level: + outstr += "\n[annotationSet] {} annotation sets\n".format( + len(sent.annotationSet) + ) + outstr += f"\n[POS] {len(sent.POS)} tags\n" + outstr += f"\n[POS_tagset] {sent.POS_tagset}\n" + outstr += "\n[GF] {} relation{}\n".format( + len(sent.GF), "s" if len(sent.GF) != 1 else "" + ) + outstr += "\n[PT] {} phrase{}\n".format( + len(sent.PT), "s" if len(sent.PT) != 1 else "" + ) + """ + Special Layers + -------------- + + The 'NER' layer contains, for some of the data, named entity labels. + + The 'WSL' (word status layer) contains, for some of the data, + spans which should not in principle be considered targets (NT). + + The 'Other' layer records relative clause constructions (Rel=relativizer, Ant=antecedent), + pleonastic 'it' (Null), and existential 'there' (Exist). + On occasion they are duplicated by accident (e.g., annotationSet 1467275 in lu6700.xml). + + The 'Sent' layer appears to contain labels that the annotator has flagged the + sentence with for their convenience: values include + 'sense1', 'sense2', 'sense3', etc.; + 'Blend', 'Canonical', 'Idiom', 'Metaphor', 'Special-Sent', + 'keepS', 'deleteS', 'reexamine' + (sometimes they are duplicated for no apparent reason). + + The POS-specific layers may contain the following kinds of spans: + Asp (aspectual particle), Non-Asp (non-aspectual particle), + Cop (copula), Supp (support), Ctrlr (controller), + Gov (governor), X. Gov and X always cooccur. + + >>> from nltk.corpus import framenet as fn + >>> def f(luRE, lyr, ignore=set()): + ... for i,ex in enumerate(fn.exemplars(luRE)): + ... if lyr in ex and ex[lyr] and set(zip(*ex[lyr])[2]) - ignore: + ... print(i,ex[lyr]) + + - Verb: Asp, Non-Asp + - Noun: Cop, Supp, Ctrlr, Gov, X + - Adj: Cop, Supp, Ctrlr, Gov, X + - Prep: Cop, Supp, Ctrlr + - Adv: Ctrlr + - Scon: (none) + - Art: (none) + """ + for lyr in ("NER", "WSL", "Other", "Sent"): + if lyr in sent and sent[lyr]: + outstr += "\n[{}] {} entr{}\n".format( + lyr, len(sent[lyr]), "ies" if len(sent[lyr]) != 1 else "y" + ) + outstr += "\n[text] + [Target] + [FE]" + # POS-specific layers: syntactically important words that are neither the target + # nor the FEs. Include these along with the first FE layer but with '^' underlining. + for lyr in ("Verb", "Noun", "Adj", "Adv", "Prep", "Scon", "Art"): + if lyr in sent and sent[lyr]: + outstr += f" + [{lyr}]" + if "FE2" in sentkeys: + outstr += " + [FE2]" + if "FE3" in sentkeys: + outstr += " + [FE3]" + outstr += "\n\n" + outstr += sent._ascii() # -> _annotation_ascii() + outstr += "\n" + + return outstr + + +def _annotation_ascii(sent): + """ + Given a sentence or FE annotation set, construct the width-limited string showing + an ASCII visualization of the sentence's annotations, calling either + _annotation_ascii_frames() or _annotation_ascii_FEs() as appropriate. + This will be attached as a method to appropriate AttrDict instances + and called in the full pretty-printing of the instance. + """ + if sent._type == "fulltext_sentence" or ( + "annotationSet" in sent and len(sent.annotationSet) > 2 + ): + # a full-text sentence OR sentence with multiple targets. + # (multiple targets = >2 annotation sets, because the first annotation set is POS.) + return _annotation_ascii_frames(sent) + else: # an FE annotation set, or an LU sentence with 1 target + return _annotation_ascii_FEs(sent) + + +def _annotation_ascii_frames(sent): + """ + ASCII string rendering of the sentence along with its targets and frame names. + Called for all full-text sentences, as well as the few LU sentences with multiple + targets (e.g., fn.lu(6412).exemplars[82] has two want.v targets). + Line-wrapped to limit the display width. + """ + # list the target spans and their associated aset index + overt = [] + for a, aset in enumerate(sent.annotationSet[1:]): + for j, k in aset.Target: + indexS = f"[{a + 1}]" + if aset.status == "UNANN" or aset.LU.status == "Problem": + indexS += " " + if aset.status == "UNANN": + indexS += "!" # warning indicator that there is a frame annotation but no FE annotation + if aset.LU.status == "Problem": + indexS += "?" # warning indicator that there is a missing LU definition (because the LU has Problem status) + overt.append((j, k, aset.LU.frame.name, indexS)) + overt = sorted(overt) + + duplicates = set() + for o, (j, k, fname, asetIndex) in enumerate(overt): + if o > 0 and j <= overt[o - 1][1]: + # multiple annotation sets on the same target + # (e.g. due to a coordination construction or multiple annotators) + if ( + overt[o - 1][:2] == (j, k) and overt[o - 1][2] == fname + ): # same target, same frame + # splice indices together + combinedIndex = ( + overt[o - 1][3] + asetIndex + ) # e.g., '[1][2]', '[1]! [2]' + combinedIndex = combinedIndex.replace(" !", "! ").replace(" ?", "? ") + overt[o - 1] = overt[o - 1][:3] + (combinedIndex,) + duplicates.add(o) + else: # different frames, same or overlapping targets + s = sent.text + for j, k, fname, asetIndex in overt: + s += "\n" + asetIndex + " " + sent.text[j:k] + " :: " + fname + s += "\n(Unable to display sentence with targets marked inline due to overlap)" + return s + for o in reversed(sorted(duplicates)): + del overt[o] + + s0 = sent.text + s1 = "" + s11 = "" + s2 = "" + i = 0 + adjust = 0 + fAbbrevs = OrderedDict() + for j, k, fname, asetIndex in overt: + if not j >= i: + assert j >= i, ( + "Overlapping targets?" + + ( + " UNANN" + if any(aset.status == "UNANN" for aset in sent.annotationSet[1:]) + else "" + ), + (j, k, asetIndex), + ) + s1 += " " * (j - i) + "*" * (k - j) + short = fname[: k - j] + if (k - j) < len(fname): + r = 0 + while short in fAbbrevs: + if fAbbrevs[short] == fname: + break + r += 1 + short = fname[: k - j - 1] + str(r) + else: # short not in fAbbrevs + fAbbrevs[short] = fname + s11 += " " * (j - i) + short.ljust(k - j) + if len(asetIndex) > (k - j): + # add space in the sentence to make room for the annotation index + amt = len(asetIndex) - (k - j) + s0 = ( + s0[: k + adjust] + "~" * amt + s0[k + adjust :] + ) # '~' to prevent line wrapping + s1 = s1[: k + adjust] + " " * amt + s1[k + adjust :] + s11 = s11[: k + adjust] + " " * amt + s11[k + adjust :] + adjust += amt + s2 += " " * (j - i) + asetIndex.ljust(k - j) + i = k + + long_lines = [s0, s1, s11, s2] + + outstr = "\n\n".join( + map("\n".join, zip_longest(*mimic_wrap(long_lines), fillvalue=" ")) + ).replace("~", " ") + outstr += "\n" + if fAbbrevs: + outstr += " (" + ", ".join("=".join(pair) for pair in fAbbrevs.items()) + ")" + assert len(fAbbrevs) == len(dict(fAbbrevs)), "Abbreviation clash" + + return outstr + + +def _annotation_ascii_FE_layer(overt, ni, feAbbrevs): + """Helper for _annotation_ascii_FEs().""" + s1 = "" + s2 = "" + i = 0 + for j, k, fename in overt: + s1 += " " * (j - i) + ("^" if fename.islower() else "-") * (k - j) + short = fename[: k - j] + if len(fename) > len(short): + r = 0 + while short in feAbbrevs: + if feAbbrevs[short] == fename: + break + r += 1 + short = fename[: k - j - 1] + str(r) + else: # short not in feAbbrevs + feAbbrevs[short] = fename + s2 += " " * (j - i) + short.ljust(k - j) + i = k + + sNI = "" + if ni: + sNI += " [" + ", ".join(":".join(x) for x in sorted(ni.items())) + "]" + return [s1, s2, sNI] + + +def _annotation_ascii_FEs(sent): + """ + ASCII string rendering of the sentence along with a single target and its FEs. + Secondary and tertiary FE layers are included if present. + 'sent' can be an FE annotation set or an LU sentence with a single target. + Line-wrapped to limit the display width. + """ + feAbbrevs = OrderedDict() + posspec = [] # POS-specific layer spans (e.g., Supp[ort], Cop[ula]) + posspec_separate = False + for lyr in ("Verb", "Noun", "Adj", "Adv", "Prep", "Scon", "Art"): + if lyr in sent and sent[lyr]: + for a, b, lbl in sent[lyr]: + if ( + lbl == "X" + ): # skip this, which covers an entire phrase typically containing the target and all its FEs + # (but do display the Gov) + continue + if any(1 for x, y, felbl in sent.FE[0] if x <= a < y or a <= x < b): + # overlap between one of the POS-specific layers and first FE layer + posspec_separate = ( + True # show POS-specific layers on a separate line + ) + posspec.append( + (a, b, lbl.lower().replace("-", "")) + ) # lowercase Cop=>cop, Non-Asp=>nonasp, etc. to distinguish from FE names + if posspec_separate: + POSSPEC = _annotation_ascii_FE_layer(posspec, {}, feAbbrevs) + FE1 = _annotation_ascii_FE_layer( + sorted(sent.FE[0] + (posspec if not posspec_separate else [])), + sent.FE[1], + feAbbrevs, + ) + FE2 = FE3 = None + if "FE2" in sent: + FE2 = _annotation_ascii_FE_layer(sent.FE2[0], sent.FE2[1], feAbbrevs) + if "FE3" in sent: + FE3 = _annotation_ascii_FE_layer(sent.FE3[0], sent.FE3[1], feAbbrevs) + + for i, j in sent.Target: + FE1span, FE1name, FE1exp = FE1 + if len(FE1span) < j: + FE1span += " " * (j - len(FE1span)) + if len(FE1name) < j: + FE1name += " " * (j - len(FE1name)) + FE1[1] = FE1name + FE1[0] = ( + FE1span[:i] + FE1span[i:j].replace(" ", "*").replace("-", "=") + FE1span[j:] + ) + long_lines = [sent.text] + if posspec_separate: + long_lines.extend(POSSPEC[:2]) + long_lines.extend([FE1[0], FE1[1] + FE1[2]]) # lines with no length limit + if FE2: + long_lines.extend([FE2[0], FE2[1] + FE2[2]]) + if FE3: + long_lines.extend([FE3[0], FE3[1] + FE3[2]]) + long_lines.append("") + outstr = "\n".join( + map("\n".join, zip_longest(*mimic_wrap(long_lines), fillvalue=" ")) + ) + if feAbbrevs: + outstr += "(" + ", ".join("=".join(pair) for pair in feAbbrevs.items()) + ")" + assert len(feAbbrevs) == len(dict(feAbbrevs)), "Abbreviation clash" + outstr += "\n" + + return outstr + + +def _pretty_fe(fe): + """ + Helper function for pretty-printing a frame element. + + :param fe: The frame element to be printed. + :type fe: AttrDict + :return: A nicely formatted string representation of the frame element. + :rtype: str + """ + fekeys = fe.keys() + outstr = "" + outstr += "frame element ({0.ID}): {0.name}\n of {1.name}({1.ID})\n".format( + fe, fe.frame + ) + if "definition" in fekeys: + outstr += "[definition]\n" + outstr += _pretty_longstring(fe.definition, " ") + if "abbrev" in fekeys: + outstr += f"[abbrev] {fe.abbrev}\n" + if "coreType" in fekeys: + outstr += f"[coreType] {fe.coreType}\n" + if "requiresFE" in fekeys: + outstr += "[requiresFE] " + if fe.requiresFE is None: + outstr += "\n" + else: + outstr += f"{fe.requiresFE.name}({fe.requiresFE.ID})\n" + if "excludesFE" in fekeys: + outstr += "[excludesFE] " + if fe.excludesFE is None: + outstr += "\n" + else: + outstr += f"{fe.excludesFE.name}({fe.excludesFE.ID})\n" + if "semType" in fekeys: + outstr += "[semType] " + if fe.semType is None: + outstr += "\n" + else: + outstr += "\n " + f"{fe.semType.name}({fe.semType.ID})" + "\n" + + return outstr + + +def _pretty_frame(frame): + """ + Helper function for pretty-printing a frame. + + :param frame: The frame to be printed. + :type frame: AttrDict + :return: A nicely formatted string representation of the frame. + :rtype: str + """ + + outstr = "" + outstr += "frame ({0.ID}): {0.name}\n\n".format(frame) + outstr += f"[URL] {frame.URL}\n\n" + outstr += "[definition]\n" + outstr += _pretty_longstring(frame.definition, " ") + "\n" + + outstr += f"[semTypes] {len(frame.semTypes)} semantic types\n" + outstr += ( + " " * (len(frame.semTypes) > 0) + + ", ".join(f"{x.name}({x.ID})" for x in frame.semTypes) + + "\n" * (len(frame.semTypes) > 0) + ) + + outstr += "\n[frameRelations] {} frame relations\n".format( + len(frame.frameRelations) + ) + outstr += " " + "\n ".join(repr(frel) for frel in frame.frameRelations) + "\n" + + outstr += f"\n[lexUnit] {len(frame.lexUnit)} lexical units\n" + lustrs = [] + for luName, lu in sorted(frame.lexUnit.items()): + tmpstr = f"{luName} ({lu.ID})" + lustrs.append(tmpstr) + outstr += "{}\n".format(_pretty_longstring(", ".join(lustrs), prefix=" ")) + + outstr += f"\n[FE] {len(frame.FE)} frame elements\n" + fes = {} + for feName, fe in sorted(frame.FE.items()): + try: + fes[fe.coreType].append(f"{feName} ({fe.ID})") + except KeyError: + fes[fe.coreType] = [] + fes[fe.coreType].append(f"{feName} ({fe.ID})") + for ct in sorted( + fes.keys(), + key=lambda ct2: [ + "Core", + "Core-Unexpressed", + "Peripheral", + "Extra-Thematic", + ].index(ct2), + ): + outstr += "{:>16}: {}\n".format(ct, ", ".join(sorted(fes[ct]))) + + outstr += "\n[FEcoreSets] {} frame element core sets\n".format( + len(frame.FEcoreSets) + ) + outstr += ( + " " + + "\n ".join( + ", ".join([x.name for x in coreSet]) for coreSet in frame.FEcoreSets + ) + + "\n" + ) + + return outstr + + +class FramenetError(Exception): + """An exception class for framenet-related errors.""" + + +def _reject_unsafe_path_component(value, kind): + """Reject a corpus-/caller-supplied name that could escape the corpus root. + + ``doc()``, ``frame_by_name()`` and ``_lu_file()`` interpolate a name into an + XML file path (CWE-22). Besides POSIX/Windows separators and ``..``, this + also rejects a Windows drive- or UNC-qualified name such as ``C:evil`` or + ``\\\\host\\share``: it contains no separator, yet ``os.path.join`` discards + the corpus root when the name carries a drive on Windows. + ``ntpath.splitdrive`` is used rather than ``os.path.splitdrive`` so the + check applies on every platform (and stays testable off Windows). + """ + value = str(value) + if ( + os.sep in value + or "/" in value + or "\\" in value + or ".." in value + or ntpath.splitdrive(value)[0] + ): + raise FramenetError(f"Invalid {kind}: {value!r}") + + +class AttrDict(dict): + """A class that wraps a dict and allows accessing the keys of the + dict as if they were attributes. Taken from here: + https://stackoverflow.com/a/14620633/8879 + + >>> foo = {'a':1, 'b':2, 'c':3} + >>> bar = AttrDict(foo) + >>> pprint(dict(bar)) + {'a': 1, 'b': 2, 'c': 3} + >>> bar.b + 2 + >>> bar.d = 4 + >>> pprint(dict(bar)) + {'a': 1, 'b': 2, 'c': 3, 'd': 4} + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # self.__dict__ = self + + def __setattr__(self, name, value): + self[name] = value + + def __getattr__(self, name): + if name == "_short_repr": + return self._short_repr + return self[name] + + def __getitem__(self, name): + v = super().__getitem__(name) + if isinstance(v, Future): + return v._data() + return v + + def _short_repr(self): + if "_type" in self: + if self["_type"].endswith("relation"): + return self.__repr__() + try: + return "<{} ID={} name={}>".format( + self["_type"], self["ID"], self["name"] + ) + except KeyError: + try: # no ID--e.g., for _type=lusubcorpus + return "<{} name={}>".format(self["_type"], self["name"]) + except KeyError: # no name--e.g., for _type=lusentence + return "<{} ID={}>".format(self["_type"], self["ID"]) + else: + return self.__repr__() + + def _str(self): + outstr = "" + + if "_type" not in self: + outstr = _pretty_any(self) + elif self["_type"] == "frame": + outstr = _pretty_frame(self) + elif self["_type"] == "fe": + outstr = _pretty_fe(self) + elif self["_type"] == "lu": + outstr = _pretty_lu(self) + elif self["_type"] == "luexemplars": # list of ALL exemplars for LU + outstr = _pretty_exemplars(self, self[0].LU) + elif ( + self["_type"] == "fulltext_annotation" + ): # list of all sentences for full-text doc + outstr = _pretty_fulltext_sentences(self) + elif self["_type"] == "lusentence": + outstr = _pretty_annotation(self) + elif self["_type"] == "fulltext_sentence": + outstr = _pretty_fulltext_sentence(self) + elif self["_type"] in ("luannotationset", "fulltext_annotationset"): + outstr = _pretty_annotation(self, aset_level=True) + elif self["_type"] == "posannotationset": + outstr = _pretty_pos(self) + elif self["_type"] == "semtype": + outstr = _pretty_semtype(self) + elif self["_type"] == "framerelationtype": + outstr = _pretty_frame_relation_type(self) + elif self["_type"] == "framerelation": + outstr = _pretty_frame_relation(self) + elif self["_type"] == "ferelation": + outstr = _pretty_fe_relation(self) + else: + outstr = _pretty_any(self) + + # ensure result is unicode string prior to applying the + # decorator (because non-ASCII characters + # could in principle occur in the data and would trigger an encoding error when + # passed as arguments to str.format()). + # assert isinstance(outstr, unicode) # not in Python 3.2 + return outstr + + def __str__(self): + return self._str() + + def __repr__(self): + return self.__str__() + + +class SpecialList(list): + """ + A list subclass which adds a '_type' attribute for special printing + (similar to an AttrDict, though this is NOT an AttrDict subclass). + """ + + def __init__(self, typ, *args, **kwargs): + super().__init__(*args, **kwargs) + self._type = typ + + def _str(self): + outstr = "" + + assert self._type + if len(self) == 0: + outstr = "[]" + elif self._type == "luexemplars": # list of ALL exemplars for LU + outstr = _pretty_exemplars(self, self[0].LU) + else: + assert False, self._type + return outstr + + def __str__(self): + return self._str() + + def __repr__(self): + return self.__str__() + + +class Future: + """ + Wraps and acts as a proxy for a value to be loaded lazily (on demand). + Adapted from https://gist.github.com/sergey-miryanov/2935416 + """ + + def __init__(self, loader, *args, **kwargs): + """ + :param loader: when called with no arguments, returns the value to be stored + :type loader: callable + """ + super().__init__(*args, **kwargs) + self._loader = loader + self._d = None + + def _data(self): + if callable(self._loader): + self._d = self._loader() + self._loader = None # the data is now cached + return self._d + + def __nonzero__(self): + return bool(self._data()) + + def __len__(self): + return len(self._data()) + + def __setitem__(self, key, value): + return self._data().__setitem__(key, value) + + def __getitem__(self, key): + return self._data().__getitem__(key) + + def __getattr__(self, key): + return self._data().__getattr__(key) + + def __str__(self): + return self._data().__str__() + + def __repr__(self): + return self._data().__repr__() + + +class PrettyDict(AttrDict): + """ + Displays an abbreviated repr of values where possible. + Inherits from AttrDict, so a callable value will + be lazily converted to an actual value. + """ + + def __init__(self, *args, **kwargs): + _BREAK_LINES = kwargs.pop("breakLines", False) + super().__init__(*args, **kwargs) + dict.__setattr__(self, "_BREAK_LINES", _BREAK_LINES) + + def __repr__(self): + parts = [] + for k, v in sorted(self.items()): + kv = repr(k) + ": " + try: + kv += v._short_repr() + except AttributeError: + kv += repr(v) + parts.append(kv) + return "{" + (",\n " if self._BREAK_LINES else ", ").join(parts) + "}" + + +class PrettyList(list): + """ + Displays an abbreviated repr of only the first several elements, not the whole list. + """ + + # from nltk.util + def __init__(self, *args, **kwargs): + self._MAX_REPR_SIZE = kwargs.pop("maxReprSize", 60) + self._BREAK_LINES = kwargs.pop("breakLines", False) + super().__init__(*args, **kwargs) + + def __repr__(self): + """ + Return a string representation for this corpus view that is + similar to a list's representation; but if it would be more + than 60 characters long, it is truncated. + """ + pieces = [] + length = 5 + + for elt in self: + pieces.append( + elt._short_repr() + ) # key difference from inherited version: call to _short_repr() + length += len(pieces[-1]) + 2 + if self._MAX_REPR_SIZE and length > self._MAX_REPR_SIZE and len(pieces) > 2: + return "[%s, ...]" % str(",\n " if self._BREAK_LINES else ", ").join( + pieces[:-1] + ) + return "[%s]" % str(",\n " if self._BREAK_LINES else ", ").join(pieces) + + +class PrettyLazyMap(LazyMap): + """ + Displays an abbreviated repr of only the first several elements, not the whole list. + """ + + # from nltk.util + _MAX_REPR_SIZE = 60 + + def __repr__(self): + """ + Return a string representation for this corpus view that is + similar to a list's representation; but if it would be more + than 60 characters long, it is truncated. + """ + pieces = [] + length = 5 + for elt in self: + pieces.append( + elt._short_repr() + ) # key difference from inherited version: call to _short_repr() + length += len(pieces[-1]) + 2 + if length > self._MAX_REPR_SIZE and len(pieces) > 2: + return "[%s, ...]" % ", ".join(pieces[:-1]) + return "[%s]" % ", ".join(pieces) + + +class PrettyLazyIteratorList(LazyIteratorList): + """ + Displays an abbreviated repr of only the first several elements, not the whole list. + """ + + # from nltk.util + _MAX_REPR_SIZE = 60 + + def __repr__(self): + """ + Return a string representation for this corpus view that is + similar to a list's representation; but if it would be more + than 60 characters long, it is truncated. + """ + pieces = [] + length = 5 + for elt in self: + pieces.append( + elt._short_repr() + ) # key difference from inherited version: call to _short_repr() + length += len(pieces[-1]) + 2 + if length > self._MAX_REPR_SIZE and len(pieces) > 2: + return "[%s, ...]" % ", ".join(pieces[:-1]) + return "[%s]" % ", ".join(pieces) + + +class PrettyLazyConcatenation(LazyConcatenation): + """ + Displays an abbreviated repr of only the first several elements, not the whole list. + """ + + # from nltk.util + _MAX_REPR_SIZE = 60 + + def __repr__(self): + """ + Return a string representation for this corpus view that is + similar to a list's representation; but if it would be more + than 60 characters long, it is truncated. + """ + pieces = [] + length = 5 + for elt in self: + pieces.append( + elt._short_repr() + ) # key difference from inherited version: call to _short_repr() + length += len(pieces[-1]) + 2 + if length > self._MAX_REPR_SIZE and len(pieces) > 2: + return "[%s, ...]" % ", ".join(pieces[:-1]) + return "[%s]" % ", ".join(pieces) + + def __add__(self, other): + """Return a list concatenating self with other.""" + return PrettyLazyIteratorList(itertools.chain(self, other)) + + def __radd__(self, other): + """Return a list concatenating other with self.""" + return PrettyLazyIteratorList(itertools.chain(other, self)) + + +class FramenetCorpusReader(XMLCorpusReader): + """A corpus reader for the Framenet Corpus. + + >>> from nltk.corpus import framenet as fn + >>> fn.lu(3238).frame.lexUnit['glint.v'] is fn.lu(3238) + True + >>> fn.frame_by_name('Replacing') is fn.lus('replace.v')[0].frame + True + >>> fn.lus('prejudice.n')[0].frame.frameRelations == fn.frame_relations('Partiality') + True + """ + + _bad_statuses = ["Problem"] + """ + When loading LUs for a frame, those whose status is in this list will be ignored. + Due to caching, if user code modifies this, it should do so before loading any data. + 'Problem' should always be listed for FrameNet 1.5, as these LUs are not included + in the XML index. + """ + + _warnings = False + + def warnings(self, v): + """Enable or disable warnings of data integrity issues as they are encountered. + If v is truthy, warnings will be enabled. + + (This is a function rather than just an attribute/property to ensure that if + enabling warnings is the first action taken, the corpus reader is instantiated first.) + """ + self._warnings = v + + def __init__(self, root, fileids): + XMLCorpusReader.__init__(self, root, fileids) + + # framenet corpus sub dirs + # sub dir containing the xml files for frames + self._frame_dir = "frame" + # sub dir containing the xml files for lexical units + self._lu_dir = "lu" + # sub dir containing the xml files for fulltext annotation files + self._fulltext_dir = "fulltext" + + # location of latest development version of FrameNet + self._fnweb_url = "https://framenet2.icsi.berkeley.edu/fnReports/data" + + # Indexes used for faster look-ups + self._frame_idx = None + self._cached_frames = {} # name -> ID + self._lu_idx = None + self._fulltext_idx = None + self._semtypes = None + self._freltyp_idx = None # frame relation types (Inheritance, Using, etc.) + self._frel_idx = None # frame-to-frame relation instances + self._ferel_idx = None # FE-to-FE relation instances + self._frel_f_idx = None # frame-to-frame relations associated with each frame + + self._readme = "README.txt" + + def help(self, attrname=None): + """Display help information summarizing the main methods.""" + + if attrname is not None: + return help(self.__getattribute__(attrname)) + + # No need to mention frame_by_name() or frame_by_id(), + # as it's easier to just call frame(). + # Also not mentioning lu_basic(). + + msg = """ +Citation: Nathan Schneider and Chuck Wooters (2017), +"The NLTK FrameNet API: Designing for Discoverability with a Rich Linguistic Resource". +Proceedings of EMNLP: System Demonstrations. https://arxiv.org/abs/1703.07438 + +Use the following methods to access data in FrameNet. +Provide a method name to `help()` for more information. + +FRAMES +====== + +frame() to look up a frame by its exact name or ID +frames() to get frames matching a name pattern +frames_by_lemma() to get frames containing an LU matching a name pattern +frame_ids_and_names() to get a mapping from frame IDs to names + +FRAME ELEMENTS +============== + +fes() to get frame elements (a.k.a. roles) matching a name pattern, optionally constrained + by a frame name pattern + +LEXICAL UNITS +============= + +lu() to look up an LU by its ID +lus() to get lexical units matching a name pattern, optionally constrained by frame +lu_ids_and_names() to get a mapping from LU IDs to names + +RELATIONS +========= + +frame_relation_types() to get the different kinds of frame-to-frame relations + (Inheritance, Subframe, Using, etc.). +frame_relations() to get the relation instances, optionally constrained by + frame(s) or relation type +fe_relations() to get the frame element pairs belonging to a frame-to-frame relation + +SEMANTIC TYPES +============== + +semtypes() to get the different kinds of semantic types that can be applied to + FEs, LUs, and entire frames +semtype() to look up a particular semtype by name, ID, or abbreviation +semtype_inherits() to check whether two semantic types have a subtype-supertype + relationship in the semtype hierarchy +propagate_semtypes() to apply inference rules that distribute semtypes over relations + between FEs + +ANNOTATIONS +=========== + +annotations() to get annotation sets, in which a token in a sentence is annotated + with a lexical unit in a frame, along with its frame elements and their syntactic properties; + can be constrained by LU name pattern and limited to lexicographic exemplars or full-text. + Sentences of full-text annotation can have multiple annotation sets. +sents() to get annotated sentences illustrating one or more lexical units +exemplars() to get sentences of lexicographic annotation, most of which have + just 1 annotation set; can be constrained by LU name pattern, frame, and overt FE(s) +doc() to look up a document of full-text annotation by its ID +docs() to get documents of full-text annotation that match a name pattern +docs_metadata() to get metadata about all full-text documents without loading them +ft_sents() to iterate over sentences of full-text annotation + +UTILITIES +========= + +buildindexes() loads metadata about all frames, LUs, etc. into memory to avoid + delay when one is accessed for the first time. It does not load annotations. +readme() gives the text of the FrameNet README file +warnings(True) to display corpus consistency warnings when loading data + """ + print(msg) + + def _buildframeindex(self): + # The total number of Frames in Framenet is fairly small (~1200) so + # this index should not be very large + if not self._frel_idx: + self._buildrelationindex() # always load frame relations before frames, + # otherwise weird ordering effects might result in incomplete information + self._frame_idx = {} + with XMLCorpusView( + self.abspath("frameIndex.xml"), "frameIndex/frame", self._handle_elt + ) as view: + for f in view: + self._frame_idx[f["ID"]] = f + + def _buildcorpusindex(self): + # The total number of fulltext annotated documents in Framenet + # is fairly small (~90) so this index should not be very large + self._fulltext_idx = {} + with XMLCorpusView( + self.abspath("fulltextIndex.xml"), + "fulltextIndex/corpus", + self._handle_fulltextindex_elt, + ) as view: + for doclist in view: + for doc in doclist: + self._fulltext_idx[doc.ID] = doc + + def _buildluindex(self): + # The number of LUs in Framenet is about 13,000 so this index + # should not be very large + self._lu_idx = {} + with XMLCorpusView( + self.abspath("luIndex.xml"), "luIndex/lu", self._handle_elt + ) as view: + for lu in view: + self._lu_idx[lu["ID"]] = ( + lu # populate with LU index entries. if any of these + ) + # are looked up they will be replaced by full LU objects. + + def _buildrelationindex(self): + # print('building relation index...', file=sys.stderr) + self._freltyp_idx = {} + self._frel_idx = {} + self._frel_f_idx = defaultdict(set) + self._ferel_idx = {} + + with XMLCorpusView( + self.abspath("frRelation.xml"), + "frameRelations/frameRelationType", + self._handle_framerelationtype_elt, + ) as view: + for freltyp in view: + self._freltyp_idx[freltyp.ID] = freltyp + for frel in freltyp.frameRelations: + supF = frel.superFrame = frel[freltyp.superFrameName] = Future( + (lambda fID: lambda: self.frame_by_id(fID))(frel.supID) + ) + subF = frel.subFrame = frel[freltyp.subFrameName] = Future( + (lambda fID: lambda: self.frame_by_id(fID))(frel.subID) + ) + self._frel_idx[frel.ID] = frel + self._frel_f_idx[frel.supID].add(frel.ID) + self._frel_f_idx[frel.subID].add(frel.ID) + for ferel in frel.feRelations: + ferel.superFrame = supF + ferel.subFrame = subF + ferel.superFE = Future( + (lambda fer: lambda: fer.superFrame.FE[fer.superFEName])( + ferel + ) + ) + ferel.subFE = Future( + (lambda fer: lambda: fer.subFrame.FE[fer.subFEName])(ferel) + ) + self._ferel_idx[ferel.ID] = ferel + # print('...done building relation index', file=sys.stderr) + + def _warn(self, *message, **kwargs): + if self._warnings: + kwargs.setdefault("file", sys.stderr) + print(*message, **kwargs) + + def buildindexes(self): + """ + Build the internal indexes to make look-ups faster. + """ + # Frames + self._buildframeindex() + # LUs + self._buildluindex() + # Fulltext annotation corpora index + self._buildcorpusindex() + # frame and FE relations + self._buildrelationindex() + + def doc(self, fn_docid): + """ + Returns the annotated document whose id number is + ``fn_docid``. This id number can be obtained by calling the + Documents() function. + + The dict that is returned from this function will contain the + following keys: + + - '_type' : 'fulltextannotation' + - 'sentence' : a list of sentences in the document + - Each item in the list is a dict containing the following keys: + - 'ID' : the ID number of the sentence + - '_type' : 'sentence' + - 'text' : the text of the sentence + - 'paragNo' : the paragraph number + - 'sentNo' : the sentence number + - 'docID' : the document ID number + - 'corpID' : the corpus ID number + - 'aPos' : the annotation position + - 'annotationSet' : a list of annotation layers for the sentence + - Each item in the list is a dict containing the following keys: + - 'ID' : the ID number of the annotation set + - '_type' : 'annotationset' + - 'status' : either 'MANUAL' or 'UNANN' + - 'luName' : (only if status is 'MANUAL') + - 'luID' : (only if status is 'MANUAL') + - 'frameID' : (only if status is 'MANUAL') + - 'frameName': (only if status is 'MANUAL') + - 'layer' : a list of labels for the layer + - Each item in the layer is a dict containing the following keys: + - '_type': 'layer' + - 'rank' + - 'name' + - 'label' : a list of labels in the layer + - Each item is a dict containing the following keys: + - 'start' + - 'end' + - 'name' + - 'feID' (optional) + + :param fn_docid: The Framenet id number of the document + :type fn_docid: int + :return: Information about the annotated document + :rtype: dict + """ + try: + xmlfname = self._fulltext_idx[fn_docid].filename + except TypeError: # happens when self._fulltext_idx == None + # build the index + self._buildcorpusindex() + xmlfname = self._fulltext_idx[fn_docid].filename + except KeyError as e: # probably means that fn_docid was not in the index + raise FramenetError(f"Unknown document id: {fn_docid}") from e + + # Security (CWE-22): defend against a malicious corpus index whose + # filename field contains path-traversal sequences. Reject the unsafe + # name and resolve the path through self.abspath() so the file is read + # via the PathPointer / nltk.pathsec sandbox instead of the builtin + # open() that a bare string path would use in XMLCorpusView. + _reject_unsafe_path_component(xmlfname, "document filename") + + # construct the path name for the xml file containing the document info + locpath = self.abspath(os.path.join(self._fulltext_dir, xmlfname)) + + # Grab the top-level xml element containing the fulltext annotation + with XMLCorpusView(locpath, "fullTextAnnotation") as view: + elt = view[0] + info = self._handle_fulltextannotation_elt(elt) + # add metadata + for k, v in self._fulltext_idx[fn_docid].items(): + info[k] = v + return info + + def frame_by_id(self, fn_fid, ignorekeys=[]): + """ + Get the details for the specified Frame using the frame's id + number. + + Usage examples: + + >>> from nltk.corpus import framenet as fn + >>> f = fn.frame_by_id(256) + >>> f.ID + 256 + >>> f.name + 'Medical_specialties' + >>> f.definition # doctest: +NORMALIZE_WHITESPACE + "This frame includes words that name medical specialties and is closely related to the + Medical_professionals frame. The FE Type characterizing a sub-are in a Specialty may also be + expressed. 'Ralph practices paediatric oncology.'" + + :param fn_fid: The Framenet id number of the frame + :type fn_fid: int + :param ignorekeys: The keys to ignore. These keys will not be + included in the output. (optional) + :type ignorekeys: list(str) + :return: Information about a frame + :rtype: dict + + Also see the ``frame()`` function for details about what is + contained in the dict that is returned. + """ + + # get the name of the frame with this id number + try: + fentry = self._frame_idx[fn_fid] + if "_type" in fentry: + return fentry # full frame object is cached + name = fentry["name"] + except TypeError: + self._buildframeindex() + name = self._frame_idx[fn_fid]["name"] + except KeyError as e: + raise FramenetError(f"Unknown frame id: {fn_fid}") from e + + return self.frame_by_name(name, ignorekeys, check_cache=False) + + def frame_by_name(self, fn_fname, ignorekeys=[], check_cache=True): + """ + Get the details for the specified Frame using the frame's name. + + Usage examples: + + >>> from nltk.corpus import framenet as fn + >>> f = fn.frame_by_name('Medical_specialties') + >>> f.ID + 256 + >>> f.name + 'Medical_specialties' + >>> f.definition # doctest: +NORMALIZE_WHITESPACE + "This frame includes words that name medical specialties and is closely related to the + Medical_professionals frame. The FE Type characterizing a sub-are in a Specialty may also be + expressed. 'Ralph practices paediatric oncology.'" + + :param fn_fname: The name of the frame + :type fn_fname: str + :param ignorekeys: The keys to ignore. These keys will not be + included in the output. (optional) + :type ignorekeys: list(str) + :return: Information about a frame + :rtype: dict + + Also see the ``frame()`` function for details about what is + contained in the dict that is returned. + """ + + if check_cache and fn_fname in self._cached_frames: + return self._frame_idx[self._cached_frames[fn_fname]] + elif not self._frame_idx: + self._buildframeindex() + + # Security (CWE-22): the frame name is interpolated into the XML file + # path. Reject crafted names, then resolve through self.abspath() so the + # file is read via the PathPointer / nltk.pathsec sandbox rather than the + # builtin open() that a bare string path would use in XMLCorpusView. + _reject_unsafe_path_component(fn_fname, "frame name") + + # construct the path name for the xml file containing the Frame info + # Grab the xml for the frame + try: + locpath = self.abspath(os.path.join(self._frame_dir, fn_fname + ".xml")) + with XMLCorpusView(locpath, "frame") as view: + elt = view[0] + except OSError as e: + raise FramenetError(f"Unknown frame: {fn_fname}") from e + + fentry = self._handle_frame_elt(elt, ignorekeys) + assert fentry + + fentry.URL = self._fnweb_url + "/" + self._frame_dir + "/" + fn_fname + ".xml" + + # INFERENCE RULE: propagate lexical semtypes from the frame to all its LUs + for st in fentry.semTypes: + if st.rootType.name == "Lexical_type": + for lu in fentry.lexUnit.values(): + if not any( + x is st for x in lu.semTypes + ): # identity containment check + lu.semTypes.append(st) + + self._frame_idx[fentry.ID] = fentry + self._cached_frames[fentry.name] = fentry.ID + """ + # now set up callables to resolve the LU pointers lazily. + # (could also do this here--caching avoids infinite recursion.) + for luName,luinfo in fentry.lexUnit.items(): + fentry.lexUnit[luName] = (lambda luID: Future(lambda: self.lu(luID)))(luinfo.ID) + """ + return fentry + + def frame(self, fn_fid_or_fname, ignorekeys=[]): + """ + Get the details for the specified Frame using the frame's name + or id number. + + Usage examples: + + >>> from nltk.corpus import framenet as fn + >>> f = fn.frame(256) + >>> f.name + 'Medical_specialties' + >>> f = fn.frame('Medical_specialties') + >>> f.ID + 256 + >>> # ensure non-ASCII character in definition doesn't trigger an encoding error: + >>> fn.frame('Imposing_obligation') # doctest: +ELLIPSIS + frame (1494): Imposing_obligation... + + + The dict that is returned from this function will contain the + following information about the Frame: + + - 'name' : the name of the Frame (e.g. 'Birth', 'Apply_heat', etc.) + - 'definition' : textual definition of the Frame + - 'ID' : the internal ID number of the Frame + - 'semTypes' : a list of semantic types for this frame + - Each item in the list is a dict containing the following keys: + - 'name' : can be used with the semtype() function + - 'ID' : can be used with the semtype() function + + - 'lexUnit' : a dict containing all of the LUs for this frame. + The keys in this dict are the names of the LUs and + the value for each key is itself a dict containing + info about the LU (see the lu() function for more info.) + + - 'FE' : a dict containing the Frame Elements that are part of this frame + The keys in this dict are the names of the FEs (e.g. 'Body_system') + and the values are dicts containing the following keys + + - 'definition' : The definition of the FE + - 'name' : The name of the FE e.g. 'Body_system' + - 'ID' : The id number + - '_type' : 'fe' + - 'abbrev' : Abbreviation e.g. 'bod' + - 'coreType' : one of "Core", "Peripheral", or "Extra-Thematic" + - 'semType' : if not None, a dict with the following two keys: + - 'name' : name of the semantic type. can be used with + the semtype() function + - 'ID' : id number of the semantic type. can be used with + the semtype() function + - 'requiresFE' : if not None, a dict with the following two keys: + - 'name' : the name of another FE in this frame + - 'ID' : the id of the other FE in this frame + - 'excludesFE' : if not None, a dict with the following two keys: + - 'name' : the name of another FE in this frame + - 'ID' : the id of the other FE in this frame + + - 'frameRelation' : a list of objects describing frame relations + - 'FEcoreSets' : a list of Frame Element core sets for this frame + - Each item in the list is a list of FE objects + + :param fn_fid_or_fname: The Framenet name or id number of the frame + :type fn_fid_or_fname: int or str + :param ignorekeys: The keys to ignore. These keys will not be + included in the output. (optional) + :type ignorekeys: list(str) + :return: Information about a frame + :rtype: dict + """ + + # get the frame info by name or id number + if isinstance(fn_fid_or_fname, str): + f = self.frame_by_name(fn_fid_or_fname, ignorekeys) + else: + f = self.frame_by_id(fn_fid_or_fname, ignorekeys) + + return f + + def frames_by_lemma(self, pat): + """ + Returns a list of all frames that contain LUs in which the + ``name`` attribute of the LU matches the given regular expression + ``pat``. Note that LU names are composed of "lemma.POS", where + the "lemma" part can be made up of either a single lexeme + (e.g. 'run') or multiple lexemes (e.g. 'a little'). + + Note: if you are going to be doing a lot of this type of + searching, you'd want to build an index that maps from lemmas to + frames because each time frames_by_lemma() is called, it has to + search through ALL of the frame XML files in the db. + + >>> from nltk.corpus import framenet as fn + >>> from nltk.corpus.reader.framenet import PrettyList + >>> PrettyList(sorted(fn.frames_by_lemma(r'(?i)a little'), key=itemgetter('ID'))) # doctest: +ELLIPSIS + [, ] + + :return: A list of frame objects. + :rtype: list(AttrDict) + """ + return PrettyList( + f + for f in self.frames() + if any(re.search(pat, luName) for luName in f.lexUnit) + ) + + def lu_basic(self, fn_luid): + """ + Returns basic information about the LU whose id is + ``fn_luid``. This is basically just a wrapper around the + ``lu()`` function with "subCorpus" info excluded. + + >>> from nltk.corpus import framenet as fn + >>> lu = PrettyDict(fn.lu_basic(256), breakLines=True) + >>> # ellipses account for differences between FN 1.5 and 1.7 + >>> lu # doctest: +ELLIPSIS + {'ID': 256, + 'POS': 'V', + 'URL': 'https://framenet2.icsi.berkeley.edu/fnReports/data/lu/lu256.xml', + '_type': 'lu', + 'cBy': ..., + 'cDate': '02/08/2001 01:27:50 PST Thu', + 'definition': 'COD: be aware of beforehand; predict.', + 'definitionMarkup': 'COD: be aware of beforehand; predict.', + 'frame': , + 'lemmaID': 15082, + 'lexemes': [{'POS': 'V', 'breakBefore': 'false', 'headword': 'false', 'name': 'foresee', 'order': 1}], + 'name': 'foresee.v', + 'semTypes': [], + 'sentenceCount': {'annotated': ..., 'total': ...}, + 'status': 'FN1_Sent'} + + :param fn_luid: The id number of the desired LU + :type fn_luid: int + :return: Basic information about the lexical unit + :rtype: dict + """ + return self.lu(fn_luid, ignorekeys=["subCorpus", "exemplars"]) + + def lu(self, fn_luid, ignorekeys=[], luName=None, frameID=None, frameName=None): + """ + Access a lexical unit by its ID. luName, frameID, and frameName are used + only in the event that the LU does not have a file in the database + (which is the case for LUs with "Problem" status); in this case, + a placeholder LU is created which just contains its name, ID, and frame. + + + Usage examples: + + >>> from nltk.corpus import framenet as fn + >>> fn.lu(256).name + 'foresee.v' + >>> fn.lu(256).definition + 'COD: be aware of beforehand; predict.' + >>> fn.lu(256).frame.name + 'Expectation' + >>> list(map(PrettyDict, fn.lu(256).lexemes)) + [{'POS': 'V', 'breakBefore': 'false', 'headword': 'false', 'name': 'foresee', 'order': 1}] + + >>> fn.lu(227).exemplars[23] # doctest: +NORMALIZE_WHITESPACE + exemplar sentence (352962): + [sentNo] 0 + [aPos] 59699508 + + [LU] (227) guess.v in Coming_to_believe + + [frame] (23) Coming_to_believe + + [annotationSet] 2 annotation sets + + [POS] 18 tags + + [POS_tagset] BNC + + [GF] 3 relations + + [PT] 3 phrases + + [Other] 1 entry + + [text] + [Target] + [FE] + + When he was inside the house , Culley noticed the characteristic + ------------------ + Content + + he would n't have guessed at . + -- ******* -- + Co C1 [Evidence:INI] + (Co=Cognizer, C1=Content) + + + + The dict that is returned from this function will contain most of the + following information about the LU. Note that some LUs do not contain + all of these pieces of information - particularly 'totalAnnotated' and + 'incorporatedFE' may be missing in some LUs: + + - 'name' : the name of the LU (e.g. 'merger.n') + - 'definition' : textual definition of the LU + - 'ID' : the internal ID number of the LU + - '_type' : 'lu' + - 'status' : e.g. 'Created' + - 'frame' : Frame that this LU belongs to + - 'POS' : the part of speech of this LU (e.g. 'N') + - 'totalAnnotated' : total number of examples annotated with this LU + - 'incorporatedFE' : FE that incorporates this LU (e.g. 'Ailment') + - 'sentenceCount' : a dict with the following two keys: + - 'annotated': number of sentences annotated with this LU + - 'total' : total number of sentences with this LU + + - 'lexemes' : a list of dicts describing the lemma of this LU. + Each dict in the list contains these keys: + + - 'POS' : part of speech e.g. 'N' + - 'name' : either single-lexeme e.g. 'merger' or + multi-lexeme e.g. 'a little' + - 'order': the order of the lexeme in the lemma (starting from 1) + - 'headword': a boolean ('true' or 'false') + - 'breakBefore': Can this lexeme be separated from the previous lexeme? + Consider: "take over.v" as in:: + + Germany took over the Netherlands in 2 days. + Germany took the Netherlands over in 2 days. + + In this case, 'breakBefore' would be "true" for the lexeme + "over". Contrast this with "take after.v" as in:: + + Mary takes after her grandmother. + *Mary takes her grandmother after. + + In this case, 'breakBefore' would be "false" for the lexeme "after" + + - 'lemmaID' : Can be used to connect lemmas in different LUs + - 'semTypes' : a list of semantic type objects for this LU + - 'subCorpus' : a list of subcorpora + - Each item in the list is a dict containing the following keys: + - 'name' : + - 'sentence' : a list of sentences in the subcorpus + - each item in the list is a dict with the following keys: + - 'ID': + - 'sentNo': + - 'text': the text of the sentence + - 'aPos': + - 'annotationSet': a list of annotation sets + - each item in the list is a dict with the following keys: + - 'ID': + - 'status': + - 'layer': a list of layers + - each layer is a dict containing the following keys: + - 'name': layer name (e.g. 'BNC') + - 'rank': + - 'label': a list of labels for the layer + - each label is a dict containing the following keys: + - 'start': start pos of label in sentence 'text' (0-based) + - 'end': end pos of label in sentence 'text' (0-based) + - 'name': name of label (e.g. 'NN1') + + Under the hood, this implementation looks up the lexical unit information + in the *frame* definition file. That file does not contain + corpus annotations, so the LU files will be accessed on demand if those are + needed. In principle, valence patterns could be loaded here too, + though these are not currently supported. + + :param fn_luid: The id number of the lexical unit + :type fn_luid: int + :param ignorekeys: The keys to ignore. These keys will not be + included in the output. (optional) + :type ignorekeys: list(str) + :return: All information about the lexical unit + :rtype: dict + """ + # look for this LU in cache + if not self._lu_idx: + self._buildluindex() + OOV = object() + luinfo = self._lu_idx.get(fn_luid, OOV) + if luinfo is OOV: + # LU not in the index. We create a placeholder by falling back to + # luName, frameID, and frameName. However, this will not be listed + # among the LUs for its frame. + self._warn( + "LU ID not found: {} ({}) in {} ({})".format( + luName, fn_luid, frameName, frameID + ) + ) + luinfo = AttrDict( + { + "_type": "lu", + "ID": fn_luid, + "name": luName, + "frameID": frameID, + "status": "Problem", + } + ) + f = self.frame_by_id(luinfo.frameID) + assert f.name == frameName, (f.name, frameName) + luinfo["frame"] = f + self._lu_idx[fn_luid] = luinfo + elif "_type" not in luinfo: + # we only have an index entry for the LU. loading the frame will replace this. + f = self.frame_by_id(luinfo.frameID) + luinfo = self._lu_idx[fn_luid] + if ignorekeys: + return AttrDict({k: v for k, v in luinfo.items() if k not in ignorekeys}) + + return luinfo + + def _lu_file(self, lu, ignorekeys=[]): + """ + Augment the LU information that was loaded from the frame file + with additional information from the LU file. + """ + fn_luid = lu.ID + + # Security (CWE-22): the LU id comes from corpus data (a + # attribute) and is interpolated into the XML file path. A non-numeric + # id can carry path-traversal sequences; reject it, then resolve through + # self.abspath() so the file is read via the PathPointer / nltk.pathsec + # sandbox rather than the builtin open() used for a bare string path. + _reject_unsafe_path_component(fn_luid, "LU id") + + fname = f"lu{fn_luid}.xml" + if not self._lu_idx: + self._buildluindex() + + try: + locpath = self.abspath(os.path.join(self._lu_dir, fname)) + with XMLCorpusView(locpath, "lexUnit") as view: + elt = view[0] + except OSError as e: + raise FramenetError(f"Unknown LU id: {fn_luid}") from e + + lu2 = self._handle_lexunit_elt(elt, ignorekeys) + lu.URL = self._fnweb_url + "/" + self._lu_dir + "/" + fname + lu.subCorpus = lu2.subCorpus + lu.exemplars = SpecialList( + "luexemplars", [sent for subc in lu.subCorpus for sent in subc.sentence] + ) + for sent in lu.exemplars: + sent["LU"] = lu + sent["frame"] = lu.frame + for aset in sent.annotationSet: + aset["LU"] = lu + aset["frame"] = lu.frame + + return lu + + def _loadsemtypes(self): + """Create the semantic types index.""" + self._semtypes = AttrDict() + with XMLCorpusView( + self.abspath("semTypes.xml"), + "semTypes/semType", + self._handle_semtype_elt, + ) as view: + for st in view: + n = st["name"] + a = st["abbrev"] + i = st["ID"] + # Both name and abbrev should be able to retrieve the + # ID. The ID will retrieve the semantic type dict itself. + self._semtypes[n] = i + self._semtypes[a] = i + self._semtypes[i] = st + # now that all individual semtype XML is loaded, we can link them together + roots = [] + for st in self.semtypes(): + if st.superType: + st.superType = self.semtype(st.superType.supID) + st.superType.subTypes.append(st) + else: + if st not in roots: + roots.append(st) + st.rootType = st + queue = list(roots) + assert queue + while queue: + st = queue.pop(0) + for child in st.subTypes: + child.rootType = st.rootType + queue.append(child) + # self.propagate_semtypes() # apply inferencing over FE relations + + def propagate_semtypes(self): + """ + Apply inference rules to distribute semtypes over relations between FEs. + For FrameNet 1.5, this results in 1011 semtypes being propagated. + (Not done by default because it requires loading all frame files, + which takes several seconds. If this needed to be fast, it could be rewritten + to traverse the neighboring relations on demand for each FE semtype.) + + >>> from nltk.corpus import framenet as fn + >>> x = sum(1 for f in fn.frames() for fe in f.FE.values() if fe.semType) + >>> fn.propagate_semtypes() + >>> y = sum(1 for f in fn.frames() for fe in f.FE.values() if fe.semType) + >>> y-x > 1000 + True + """ + if not self._semtypes: + self._loadsemtypes() + if not self._ferel_idx: + self._buildrelationindex() + changed = True + i = 0 + nPropagations = 0 + while changed: + # make a pass and see if anything needs to be propagated + i += 1 + changed = False + for ferel in self.fe_relations(): + superST = ferel.superFE.semType + subST = ferel.subFE.semType + try: + if superST and superST is not subST: + # propagate downward + assert subST is None or self.semtype_inherits(subST, superST), ( + superST.name, + ferel, + subST.name, + ) + if subST is None: + ferel.subFE.semType = subST = superST + changed = True + nPropagations += 1 + if ( + ferel.type.name in ["Perspective_on", "Subframe", "Precedes"] + and subST + and subST is not superST + ): + # propagate upward + assert superST is None, (superST.name, ferel, subST.name) + ferel.superFE.semType = superST = subST + changed = True + nPropagations += 1 + except AssertionError as ex: + # bug in the data! ignore + # print(ex, file=sys.stderr) + continue + # print(i, nPropagations, file=sys.stderr) + + def semtype(self, key): + """ + >>> from nltk.corpus import framenet as fn + >>> fn.semtype(233).name + 'Temperature' + >>> fn.semtype(233).abbrev + 'Temp' + >>> fn.semtype('Temperature').ID + 233 + + :param key: The name, abbreviation, or id number of the semantic type + :type key: string or int + :return: Information about a semantic type + :rtype: dict + """ + if isinstance(key, int): + stid = key + else: + try: + stid = self._semtypes[key] + except TypeError: + self._loadsemtypes() + stid = self._semtypes[key] + + try: + st = self._semtypes[stid] + except TypeError: + self._loadsemtypes() + st = self._semtypes[stid] + + return st + + def semtype_inherits(self, st, superST): + if not isinstance(st, dict): + st = self.semtype(st) + if not isinstance(superST, dict): + superST = self.semtype(superST) + par = st.superType + while par: + if par is superST: + return True + par = par.superType + return False + + def frames(self, name=None): + """ + Obtain details for a specific frame. + + >>> from nltk.corpus import framenet as fn + >>> len(fn.frames()) in (1019, 1221) # FN 1.5 and 1.7, resp. + True + >>> x = PrettyList(fn.frames(r'(?i)crim'), maxReprSize=0, breakLines=True) + >>> x.sort(key=itemgetter('ID')) + >>> x + [, + , + , + ] + + A brief intro to Frames (excerpted from "FrameNet II: Extended + Theory and Practice" by Ruppenhofer et. al., 2010): + + A Frame is a script-like conceptual structure that describes a + particular type of situation, object, or event along with the + participants and props that are needed for that Frame. For + example, the "Apply_heat" frame describes a common situation + involving a Cook, some Food, and a Heating_Instrument, and is + evoked by words such as bake, blanch, boil, broil, brown, + simmer, steam, etc. + + We call the roles of a Frame "frame elements" (FEs) and the + frame-evoking words are called "lexical units" (LUs). + + FrameNet includes relations between Frames. Several types of + relations are defined, of which the most important are: + + - Inheritance: An IS-A relation. The child frame is a subtype + of the parent frame, and each FE in the parent is bound to + a corresponding FE in the child. An example is the + "Revenge" frame which inherits from the + "Rewards_and_punishments" frame. + + - Using: The child frame presupposes the parent frame as + background, e.g the "Speed" frame "uses" (or presupposes) + the "Motion" frame; however, not all parent FEs need to be + bound to child FEs. + + - Subframe: The child frame is a subevent of a complex event + represented by the parent, e.g. the "Criminal_process" frame + has subframes of "Arrest", "Arraignment", "Trial", and + "Sentencing". + + - Perspective_on: The child frame provides a particular + perspective on an un-perspectivized parent frame. A pair of + examples consists of the "Hiring" and "Get_a_job" frames, + which perspectivize the "Employment_start" frame from the + Employer's and the Employee's point of view, respectively. + + :param name: A regular expression pattern used to match against + Frame names. If 'name' is None, then a list of all + Framenet Frames will be returned. + :type name: str + :return: A list of matching Frames (or all Frames). + :rtype: list(AttrDict) + """ + try: + fIDs = list(self._frame_idx.keys()) + except AttributeError: + self._buildframeindex() + fIDs = list(self._frame_idx.keys()) + + if name is not None: + return PrettyList( + self.frame(fID) for fID, finfo in self.frame_ids_and_names(name).items() + ) + else: + return PrettyLazyMap(self.frame, fIDs) + + def frame_ids_and_names(self, name=None): + """ + Uses the frame index, which is much faster than looking up each frame definition + if only the names and IDs are needed. + """ + if not self._frame_idx: + self._buildframeindex() + return { + fID: finfo.name + for fID, finfo in self._frame_idx.items() + if name is None or re.search(name, finfo.name) is not None + } + + def fes(self, name=None, frame=None): + """ + Lists frame element objects. If 'name' is provided, this is treated as + a case-insensitive regular expression to filter by frame name. + (Case-insensitivity is because casing of frame element names is not always + consistent across frames.) Specify 'frame' to filter by a frame name pattern, + ID, or object. + + >>> from nltk.corpus import framenet as fn + >>> fn.fes('Noise_maker') + [] + >>> sorted([(fe.frame.name,fe.name) for fe in fn.fes('sound')]) # doctest: +NORMALIZE_WHITESPACE + [('Cause_to_make_noise', 'Sound_maker'), ('Make_noise', 'Sound'), + ('Make_noise', 'Sound_source'), ('Sound_movement', 'Location_of_sound_source'), + ('Sound_movement', 'Sound'), ('Sound_movement', 'Sound_source'), + ('Sounds', 'Component_sound'), ('Sounds', 'Location_of_sound_source'), + ('Sounds', 'Sound_source'), ('Vocalizations', 'Location_of_sound_source'), + ('Vocalizations', 'Sound_source')] + >>> sorted([(fe.frame.name,fe.name) for fe in fn.fes('sound',r'(?i)make_noise')]) # doctest: +NORMALIZE_WHITESPACE + [('Cause_to_make_noise', 'Sound_maker'), + ('Make_noise', 'Sound'), + ('Make_noise', 'Sound_source')] + >>> sorted(set(fe.name for fe in fn.fes('^sound'))) + ['Sound', 'Sound_maker', 'Sound_source'] + >>> len(fn.fes('^sound$')) + 2 + + :param name: A regular expression pattern used to match against + frame element names. If 'name' is None, then a list of all + frame elements will be returned. + :type name: str + :return: A list of matching frame elements + :rtype: list(AttrDict) + """ + # what frames are we searching in? + if frame is not None: + if isinstance(frame, int): + frames = [self.frame(frame)] + elif isinstance(frame, str): + frames = self.frames(frame) + else: + frames = [frame] + else: + frames = self.frames() + + return PrettyList( + fe + for f in frames + for fename, fe in f.FE.items() + if name is None or re.search(name, fename, re.I) + ) + + def lus(self, name=None, frame=None): + """ + Obtain details for lexical units. + Optionally restrict by lexical unit name pattern, and/or to a certain frame + or frames whose name matches a pattern. + + >>> from nltk.corpus import framenet as fn + >>> len(fn.lus()) in (11829, 13572) # FN 1.5 and 1.7, resp. + True + >>> PrettyList(sorted(fn.lus(r'(?i)a little'), key=itemgetter('ID')), maxReprSize=0, breakLines=True) + [, + , + ] + >>> PrettyList(sorted(fn.lus(r'interest', r'(?i)stimulus'), key=itemgetter('ID'))) + [, ] + + A brief intro to Lexical Units (excerpted from "FrameNet II: + Extended Theory and Practice" by Ruppenhofer et. al., 2010): + + A lexical unit (LU) is a pairing of a word with a meaning. For + example, the "Apply_heat" Frame describes a common situation + involving a Cook, some Food, and a Heating Instrument, and is + _evoked_ by words such as bake, blanch, boil, broil, brown, + simmer, steam, etc. These frame-evoking words are the LUs in the + Apply_heat frame. Each sense of a polysemous word is a different + LU. + + We have used the word "word" in talking about LUs. The reality + is actually rather complex. When we say that the word "bake" is + polysemous, we mean that the lemma "bake.v" (which has the + word-forms "bake", "bakes", "baked", and "baking") is linked to + three different frames: + + - Apply_heat: "Michelle baked the potatoes for 45 minutes." + + - Cooking_creation: "Michelle baked her mother a cake for her birthday." + + - Absorb_heat: "The potatoes have to bake for more than 30 minutes." + + These constitute three different LUs, with different + definitions. + + Multiword expressions such as "given name" and hyphenated words + like "shut-eye" can also be LUs. Idiomatic phrases such as + "middle of nowhere" and "give the slip (to)" are also defined as + LUs in the appropriate frames ("Isolated_places" and "Evading", + respectively), and their internal structure is not analyzed. + + Framenet provides multiple annotated examples of each sense of a + word (i.e. each LU). Moreover, the set of examples + (approximately 20 per LU) illustrates all of the combinatorial + possibilities of the lexical unit. + + Each LU is linked to a Frame, and hence to the other words which + evoke that Frame. This makes the FrameNet database similar to a + thesaurus, grouping together semantically similar words. + + In the simplest case, frame-evoking words are verbs such as + "fried" in: + + "Matilde fried the catfish in a heavy iron skillet." + + Sometimes event nouns may evoke a Frame. For example, + "reduction" evokes "Cause_change_of_scalar_position" in: + + "...the reduction of debt levels to $665 million from $2.6 billion." + + Adjectives may also evoke a Frame. For example, "asleep" may + evoke the "Sleep" frame as in: + + "They were asleep for hours." + + Many common nouns, such as artifacts like "hat" or "tower", + typically serve as dependents rather than clearly evoking their + own frames. + + :param name: A regular expression pattern used to search the LU + names. Note that LU names take the form of a dotted + string (e.g. "run.v" or "a little.adv") in which a + lemma precedes the "." and a POS follows the + dot. The lemma may be composed of a single lexeme + (e.g. "run") or of multiple lexemes (e.g. "a + little"). If 'name' is not given, then all LUs will + be returned. + + The valid POSes are: + + v - verb + n - noun + a - adjective + adv - adverb + prep - preposition + num - numbers + intj - interjection + art - article + c - conjunction + scon - subordinating conjunction + + :type name: str + :type frame: str or int or frame + :return: A list of selected (or all) lexical units + :rtype: list of LU objects (dicts). See the lu() function for info + about the specifics of LU objects. + + """ + if not self._lu_idx: + self._buildluindex() + + if name is not None: # match LUs, then restrict by frame + result = PrettyList( + self.lu(luID) for luID, luName in self.lu_ids_and_names(name).items() + ) + if frame is not None: + if isinstance(frame, int): + frameIDs = {frame} + elif isinstance(frame, str): + frameIDs = {f.ID for f in self.frames(frame)} + else: + frameIDs = {frame.ID} + result = PrettyList(lu for lu in result if lu.frame.ID in frameIDs) + elif frame is not None: # all LUs in matching frames + if isinstance(frame, int): + frames = [self.frame(frame)] + elif isinstance(frame, str): + frames = self.frames(frame) + else: + frames = [frame] + result = PrettyLazyIteratorList( + iter(LazyConcatenation(list(f.lexUnit.values()) for f in frames)) + ) + else: # all LUs + luIDs = [ + luID + for luID, lu in self._lu_idx.items() + if lu.status not in self._bad_statuses + ] + result = PrettyLazyMap(self.lu, luIDs) + return result + + def lu_ids_and_names(self, name=None): + """ + Uses the LU index, which is much faster than looking up each LU definition + if only the names and IDs are needed. + """ + if not self._lu_idx: + self._buildluindex() + return { + luID: luinfo.name + for luID, luinfo in self._lu_idx.items() + if luinfo.status not in self._bad_statuses + and (name is None or re.search(name, luinfo.name) is not None) + } + + def docs_metadata(self, name=None): + """ + Return an index of the annotated documents in Framenet. + + Details for a specific annotated document can be obtained using this + class's doc() function and pass it the value of the 'ID' field. + + >>> from nltk.corpus import framenet as fn + >>> len(fn.docs()) in (78, 107) # FN 1.5 and 1.7, resp. + True + >>> set([x.corpname for x in fn.docs_metadata()])>=set(['ANC', 'KBEval', \ + 'LUCorpus-v0.3', 'Miscellaneous', 'NTI', 'PropBank']) + True + + :param name: A regular expression pattern used to search the + file name of each annotated document. The document's + file name contains the name of the corpus that the + document is from, followed by two underscores "__" + followed by the document name. So, for example, the + file name "LUCorpus-v0.3__20000410_nyt-NEW.xml" is + from the corpus named "LUCorpus-v0.3" and the + document name is "20000410_nyt-NEW.xml". + :type name: str + :return: A list of selected (or all) annotated documents + :rtype: list of dicts, where each dict object contains the following + keys: + + - 'name' + - 'ID' + - 'corpid' + - 'corpname' + - 'description' + - 'filename' + """ + try: + ftlist = PrettyList(self._fulltext_idx.values()) + except AttributeError: + self._buildcorpusindex() + ftlist = PrettyList(self._fulltext_idx.values()) + + if name is None: + return ftlist + else: + return PrettyList( + x for x in ftlist if re.search(name, x["filename"]) is not None + ) + + def docs(self, name=None): + """ + Return a list of the annotated full-text documents in FrameNet, + optionally filtered by a regex to be matched against the document name. + """ + return PrettyLazyMap((lambda x: self.doc(x.ID)), self.docs_metadata(name)) + + def sents(self, exemplars=True, full_text=True): + """ + Annotated sentences matching the specified criteria. + """ + if exemplars: + if full_text: + return self.exemplars() + self.ft_sents() + else: + return self.exemplars() + elif full_text: + return self.ft_sents() + + def annotations(self, luNamePattern=None, exemplars=True, full_text=True): + """ + Frame annotation sets matching the specified criteria. + """ + + if exemplars: + epart = PrettyLazyIteratorList( + sent.frameAnnotation for sent in self.exemplars(luNamePattern) + ) + else: + epart = [] + + if full_text: + if luNamePattern is not None: + matchedLUIDs = set(self.lu_ids_and_names(luNamePattern).keys()) + ftpart = PrettyLazyIteratorList( + aset + for sent in self.ft_sents() + for aset in sent.annotationSet[1:] + if luNamePattern is None or aset.get("luID", "CXN_ASET") in matchedLUIDs + ) + else: + ftpart = [] + + if exemplars: + if full_text: + return epart + ftpart + else: + return epart + elif full_text: + return ftpart + + def exemplars(self, luNamePattern=None, frame=None, fe=None, fe2=None): + """ + Lexicographic exemplar sentences, optionally filtered by LU name and/or 1-2 FEs that + are realized overtly. 'frame' may be a name pattern, frame ID, or frame instance. + 'fe' may be a name pattern or FE instance; if specified, 'fe2' may also + be specified to retrieve sentences with both overt FEs (in either order). + """ + if fe is None and fe2 is not None: + raise FramenetError("exemplars(..., fe=None, fe2=) is not allowed") + elif fe is not None and fe2 is not None: + if not isinstance(fe2, str): + if isinstance(fe, str): + # fe2 is specific to a particular frame. swap fe and fe2 so fe is always used to determine the frame. + fe, fe2 = fe2, fe + elif fe.frame is not fe2.frame: # ensure frames match + raise FramenetError( + "exemplars() call with inconsistent `fe` and `fe2` specification (frames must match)" + ) + if frame is None and fe is not None and not isinstance(fe, str): + frame = fe.frame + + # narrow down to frames matching criteria + + lusByFrame = defaultdict( + list + ) # frame name -> matching LUs, if luNamePattern is specified + if frame is not None or luNamePattern is not None: + if frame is None or isinstance(frame, str): + if luNamePattern is not None: + frames = set() + for lu in self.lus(luNamePattern, frame=frame): + frames.add(lu.frame.ID) + lusByFrame[lu.frame.name].append(lu) + frames = LazyMap(self.frame, list(frames)) + else: + frames = self.frames(frame) + else: + if isinstance(frame, int): + frames = [self.frame(frame)] + else: # frame object + frames = [frame] + + if luNamePattern is not None: + lusByFrame = {frame.name: self.lus(luNamePattern, frame=frame)} + + if fe is not None: # narrow to frames that define this FE + if isinstance(fe, str): + frames = PrettyLazyIteratorList( + f + for f in frames + if fe in f.FE + or any(re.search(fe, ffe, re.I) for ffe in f.FE.keys()) + ) + else: + if fe.frame not in frames: + raise FramenetError( + "exemplars() call with inconsistent `frame` and `fe` specification" + ) + frames = [fe.frame] + + if fe2 is not None: # narrow to frames that ALSO define this FE + if isinstance(fe2, str): + frames = PrettyLazyIteratorList( + f + for f in frames + if fe2 in f.FE + or any(re.search(fe2, ffe, re.I) for ffe in f.FE.keys()) + ) + # else we already narrowed it to a single frame + else: # frame, luNamePattern are None. fe, fe2 are None or strings + if fe is not None: + frames = {ffe.frame.ID for ffe in self.fes(fe)} + if fe2 is not None: + frames2 = {ffe.frame.ID for ffe in self.fes(fe2)} + frames = frames & frames2 + frames = LazyMap(self.frame, list(frames)) + else: + frames = self.frames() + + # we've narrowed down 'frames' + # now get exemplars for relevant LUs in those frames + + def _matching_exs(): + for f in frames: + fes = fes2 = None # FEs of interest + if fe is not None: + fes = ( + {ffe for ffe in f.FE.keys() if re.search(fe, ffe, re.I)} + if isinstance(fe, str) + else {fe.name} + ) + if fe2 is not None: + fes2 = ( + {ffe for ffe in f.FE.keys() if re.search(fe2, ffe, re.I)} + if isinstance(fe2, str) + else {fe2.name} + ) + + for lu in ( + lusByFrame[f.name] + if luNamePattern is not None + else f.lexUnit.values() + ): + for ex in lu.exemplars: + if (fes is None or self._exemplar_of_fes(ex, fes)) and ( + fes2 is None or self._exemplar_of_fes(ex, fes2) + ): + yield ex + + return PrettyLazyIteratorList(_matching_exs()) + + def _exemplar_of_fes(self, ex, fes=None): + """ + Given an exemplar sentence and a set of FE names, return the subset of FE names + that are realized overtly in the sentence on the FE, FE2, or FE3 layer. + + If 'fes' is None, returns all overt FE names. + """ + overtNames = set(list(zip(*ex.FE[0]))[2]) if ex.FE[0] else set() + if "FE2" in ex: + overtNames |= set(list(zip(*ex.FE2[0]))[2]) if ex.FE2[0] else set() + if "FE3" in ex: + overtNames |= set(list(zip(*ex.FE3[0]))[2]) if ex.FE3[0] else set() + return overtNames & fes if fes is not None else overtNames + + def ft_sents(self, docNamePattern=None): + """ + Full-text annotation sentences, optionally filtered by document name. + """ + return PrettyLazyIteratorList( + sent for d in self.docs(docNamePattern) for sent in d.sentence + ) + + def frame_relation_types(self): + """ + Obtain a list of frame relation types. + + >>> from nltk.corpus import framenet as fn + >>> frts = sorted(fn.frame_relation_types(), key=itemgetter('ID')) + >>> isinstance(frts, list) + True + >>> len(frts) in (9, 10) # FN 1.5 and 1.7, resp. + True + >>> PrettyDict(frts[0], breakLines=True) + {'ID': 1, + '_type': 'framerelationtype', + 'frameRelations': [ Child=Change_of_consistency>, Child=Rotting>, ...], + 'name': 'Inheritance', + 'subFrameName': 'Child', + 'superFrameName': 'Parent'} + + :return: A list of all of the frame relation types in framenet + :rtype: list(dict) + """ + if not self._freltyp_idx: + self._buildrelationindex() + return self._freltyp_idx.values() + + def frame_relations(self, frame=None, frame2=None, type=None): + """ + :param frame: (optional) frame object, name, or ID; only relations involving + this frame will be returned + :param frame2: (optional; 'frame' must be a different frame) only show relations + between the two specified frames, in either direction + :param type: (optional) frame relation type (name or object); show only relations + of this type + :type frame: int or str or AttrDict + :return: A list of all of the frame relations in framenet + :rtype: list(dict) + + >>> from nltk.corpus import framenet as fn + >>> frels = fn.frame_relations() + >>> isinstance(frels, list) + True + >>> len(frels) in (1676, 2070) # FN 1.5 and 1.7, resp. + True + >>> PrettyList(fn.frame_relations('Cooking_creation'), maxReprSize=0, breakLines=True) + [ Child=Cooking_creation>, + Child=Cooking_creation>, + ReferringEntry=Cooking_creation>] + >>> PrettyList(fn.frame_relations(274), breakLines=True) + [ Child=Dodging>, + Child=Evading>, ...] + >>> PrettyList(fn.frame_relations(fn.frame('Cooking_creation')), breakLines=True) + [ Child=Cooking_creation>, + Child=Cooking_creation>, ...] + >>> PrettyList(fn.frame_relations('Cooking_creation', type='Inheritance')) + [ Child=Cooking_creation>] + >>> PrettyList(fn.frame_relations('Cooking_creation', 'Apply_heat'), breakLines=True) # doctest: +NORMALIZE_WHITESPACE + [ Child=Cooking_creation>, + ReferringEntry=Cooking_creation>] + """ + relation_type = type + + if not self._frel_idx: + self._buildrelationindex() + + rels = None + + if relation_type is not None: + if not isinstance(relation_type, dict): + type = [rt for rt in self.frame_relation_types() if rt.name == type][0] + assert isinstance(type, dict) + + # lookup by 'frame' + if frame is not None: + if isinstance(frame, dict) and "frameRelations" in frame: + rels = PrettyList(frame.frameRelations) + else: + if not isinstance(frame, int): + if isinstance(frame, dict): + frame = frame.ID + else: + frame = self.frame_by_name(frame).ID + rels = [self._frel_idx[frelID] for frelID in self._frel_f_idx[frame]] + + # filter by 'type' + if type is not None: + rels = [rel for rel in rels if rel.type is type] + elif type is not None: + # lookup by 'type' + rels = type.frameRelations + else: + rels = self._frel_idx.values() + + # filter by 'frame2' + if frame2 is not None: + if frame is None: + raise FramenetError( + "frame_relations(frame=None, frame2=) is not allowed" + ) + if not isinstance(frame2, int): + if isinstance(frame2, dict): + frame2 = frame2.ID + else: + frame2 = self.frame_by_name(frame2).ID + if frame == frame2: + raise FramenetError( + "The two frame arguments to frame_relations() must be different frames" + ) + rels = [ + rel + for rel in rels + if rel.superFrame.ID == frame2 or rel.subFrame.ID == frame2 + ] + + return PrettyList( + sorted( + rels, + key=lambda frel: (frel.type.ID, frel.superFrameName, frel.subFrameName), + ) + ) + + def fe_relations(self): + """ + Obtain a list of frame element relations. + + >>> from nltk.corpus import framenet as fn + >>> ferels = fn.fe_relations() + >>> isinstance(ferels, list) + True + >>> len(ferels) in (10020, 12393) # FN 1.5 and 1.7, resp. + True + >>> PrettyDict(ferels[0], breakLines=True) # doctest: +NORMALIZE_WHITESPACE + {'ID': 14642, + '_type': 'ferelation', + 'frameRelation': Child=Lively_place>, + 'subFE': , + 'subFEName': 'Degree', + 'subFrame': , + 'subID': 11370, + 'supID': 2271, + 'superFE': , + 'superFEName': 'Degree', + 'superFrame': , + 'type': } + + :return: A list of all of the frame element relations in framenet + :rtype: list(dict) + """ + if not self._ferel_idx: + self._buildrelationindex() + return PrettyList( + sorted( + self._ferel_idx.values(), + key=lambda ferel: ( + ferel.type.ID, + ferel.frameRelation.superFrameName, + ferel.superFEName, + ferel.frameRelation.subFrameName, + ferel.subFEName, + ), + ) + ) + + def semtypes(self): + """ + Obtain a list of semantic types. + + >>> from nltk.corpus import framenet as fn + >>> stypes = fn.semtypes() + >>> len(stypes) in (73, 109) # FN 1.5 and 1.7, resp. + True + >>> sorted(stypes[0].keys()) + ['ID', '_type', 'abbrev', 'definition', 'definitionMarkup', 'name', 'rootType', 'subTypes', 'superType'] + + :return: A list of all of the semantic types in framenet + :rtype: list(dict) + """ + if not self._semtypes: + self._loadsemtypes() + return PrettyList( + self._semtypes[i] for i in self._semtypes if isinstance(i, int) + ) + + def _load_xml_attributes(self, d, elt): + """ + Extracts a subset of the attributes from the given element and + returns them in a dictionary. + + :param d: A dictionary in which to store the attributes. + :type d: dict + :param elt: An ElementTree Element + :type elt: Element + :return: Returns the input dict ``d`` possibly including attributes from ``elt`` + :rtype: dict + """ + + d = type(d)(d) + + try: + attr_dict = elt.attrib + except AttributeError: + return d + + if attr_dict is None: + return d + + # Ignore these attributes when loading attributes from an xml node + ignore_attrs = [ #'cBy', 'cDate', 'mDate', # <-- annotation metadata that could be of interest + "xsi", + "schemaLocation", + "xmlns", + "bgColor", + "fgColor", + ] + + for attr in attr_dict: + if any(attr.endswith(x) for x in ignore_attrs): + continue + + val = attr_dict[attr] + if val.isdigit(): + d[attr] = int(val) + else: + d[attr] = val + + return d + + def _strip_tags(self, data): + """ + Gets rid of all tags and newline characters from the given input + + :return: A cleaned-up version of the input string + :rtype: str + """ + + try: + r""" + # Look for boundary issues in markup. (Sometimes FEs are pluralized in definitions.) + m = re.search(r'\w[<][^/]|[<][/][^>]+[>](s\w|[a-rt-z0-9])', data) + if m: + print('Markup boundary:', data[max(0,m.start(0)-10):m.end(0)+10].replace('\n',' '), file=sys.stderr) + """ + + data = data.replace("", "") + data = data.replace("", "") + data = re.sub('', "", data) + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "'") + data = data.replace("", "'") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + data = data.replace("", "") + + # Get rid of and tags + data = data.replace("", "") + data = data.replace("", "") + + data = data.replace("\n", " ") + except AttributeError: + pass + + return data + + def _handle_elt(self, elt, tagspec=None): + """Extracts and returns the attributes of the given element""" + return self._load_xml_attributes(AttrDict(), elt) + + def _handle_fulltextindex_elt(self, elt, tagspec=None): + """ + Extracts corpus/document info from the fulltextIndex.xml file. + + Note that this function "flattens" the information contained + in each of the "corpus" elements, so that each "document" + element will contain attributes for the corpus and + corpusid. Also, each of the "document" items will contain a + new attribute called "filename" that is the base file name of + the xml file for the document in the "fulltext" subdir of the + Framenet corpus. + """ + ftinfo = self._load_xml_attributes(AttrDict(), elt) + corpname = ftinfo.name + corpid = ftinfo.ID + retlist = [] + for sub in elt: + if sub.tag.endswith("document"): + doc = self._load_xml_attributes(AttrDict(), sub) + if "name" in doc: + docname = doc.name + else: + docname = doc.description + doc.filename = f"{corpname}__{docname}.xml" + doc.URL = ( + self._fnweb_url + "/" + self._fulltext_dir + "/" + doc.filename + ) + doc.corpname = corpname + doc.corpid = corpid + retlist.append(doc) + + return retlist + + def _handle_frame_elt(self, elt, ignorekeys=[]): + """Load the info for a Frame from a frame xml file""" + frinfo = self._load_xml_attributes(AttrDict(), elt) + + frinfo["_type"] = "frame" + frinfo["definition"] = "" + frinfo["definitionMarkup"] = "" + frinfo["FE"] = PrettyDict() + frinfo["FEcoreSets"] = [] + frinfo["lexUnit"] = PrettyDict() + frinfo["semTypes"] = [] + for k in ignorekeys: + if k in frinfo: + del frinfo[k] + + for sub in elt: + if sub.tag.endswith("definition") and "definition" not in ignorekeys: + frinfo["definitionMarkup"] = sub.text + frinfo["definition"] = self._strip_tags(sub.text) + elif sub.tag.endswith("FE") and "FE" not in ignorekeys: + feinfo = self._handle_fe_elt(sub) + frinfo["FE"][feinfo.name] = feinfo + feinfo["frame"] = frinfo # backpointer + elif sub.tag.endswith("FEcoreSet") and "FEcoreSet" not in ignorekeys: + coreset = self._handle_fecoreset_elt(sub) + # assumes all FEs have been loaded before coresets + frinfo["FEcoreSets"].append( + PrettyList(frinfo["FE"][fe.name] for fe in coreset) + ) + elif sub.tag.endswith("lexUnit") and "lexUnit" not in ignorekeys: + luentry = self._handle_framelexunit_elt(sub) + if luentry["status"] in self._bad_statuses: + # problematic LU entry; ignore it + continue + luentry["frame"] = frinfo + luentry["URL"] = ( + self._fnweb_url + + "/" + + self._lu_dir + + "/" + + "lu{}.xml".format(luentry["ID"]) + ) + luentry["subCorpus"] = Future( + (lambda lu: lambda: self._lu_file(lu).subCorpus)(luentry) + ) + luentry["exemplars"] = Future( + (lambda lu: lambda: self._lu_file(lu).exemplars)(luentry) + ) + frinfo["lexUnit"][luentry.name] = luentry + if not self._lu_idx: + self._buildluindex() + self._lu_idx[luentry.ID] = luentry + elif sub.tag.endswith("semType") and "semTypes" not in ignorekeys: + semtypeinfo = self._load_xml_attributes(AttrDict(), sub) + frinfo["semTypes"].append(self.semtype(semtypeinfo.ID)) + + frinfo["frameRelations"] = self.frame_relations(frame=frinfo) + + # resolve 'requires' and 'excludes' links between FEs of this frame + for fe in frinfo.FE.values(): + if fe.requiresFE: + name, ID = fe.requiresFE.name, fe.requiresFE.ID + fe.requiresFE = frinfo.FE[name] + assert fe.requiresFE.ID == ID + if fe.excludesFE: + name, ID = fe.excludesFE.name, fe.excludesFE.ID + fe.excludesFE = frinfo.FE[name] + assert fe.excludesFE.ID == ID + + return frinfo + + def _handle_fecoreset_elt(self, elt): + """Load fe coreset info from xml.""" + info = self._load_xml_attributes(AttrDict(), elt) + tmp = [] + for sub in elt: + tmp.append(self._load_xml_attributes(AttrDict(), sub)) + + return tmp + + def _handle_framerelationtype_elt(self, elt, *args): + """Load frame-relation element and its child fe-relation elements from frRelation.xml.""" + info = self._load_xml_attributes(AttrDict(), elt) + info["_type"] = "framerelationtype" + info["frameRelations"] = PrettyList() + + for sub in elt: + if sub.tag.endswith("frameRelation"): + frel = self._handle_framerelation_elt(sub) + frel["type"] = info # backpointer + for ferel in frel.feRelations: + ferel["type"] = info + info["frameRelations"].append(frel) + + return info + + def _handle_framerelation_elt(self, elt): + """Load frame-relation element and its child fe-relation elements from frRelation.xml.""" + info = self._load_xml_attributes(AttrDict(), elt) + assert info["superFrameName"] != info["subFrameName"], (elt, info) + info["_type"] = "framerelation" + info["feRelations"] = PrettyList() + + for sub in elt: + if sub.tag.endswith("FERelation"): + ferel = self._handle_elt(sub) + ferel["_type"] = "ferelation" + ferel["frameRelation"] = info # backpointer + info["feRelations"].append(ferel) + + return info + + def _handle_fulltextannotation_elt(self, elt): + """Load full annotation info for a document from its xml + file. The main element (fullTextAnnotation) contains a 'header' + element (which we ignore here) and a bunch of 'sentence' + elements.""" + info = AttrDict() + info["_type"] = "fulltext_annotation" + info["sentence"] = [] + + for sub in elt: + if sub.tag.endswith("header"): + continue # not used + elif sub.tag.endswith("sentence"): + s = self._handle_fulltext_sentence_elt(sub) + s.doc = info + info["sentence"].append(s) + + return info + + def _handle_fulltext_sentence_elt(self, elt): + """Load information from the given 'sentence' element. Each + 'sentence' element contains a "text" and "annotationSet" sub + elements.""" + info = self._load_xml_attributes(AttrDict(), elt) + info["_type"] = "fulltext_sentence" + info["annotationSet"] = [] + info["targets"] = [] + target_spans = set() + info["_ascii"] = types.MethodType( + _annotation_ascii, info + ) # attach a method for this instance + info["text"] = "" + + for sub in elt: + if sub.tag.endswith("text"): + info["text"] = self._strip_tags(sub.text) + elif sub.tag.endswith("annotationSet"): + a = self._handle_fulltextannotationset_elt( + sub, is_pos=(len(info["annotationSet"]) == 0) + ) + if "cxnID" in a: # ignoring construction annotations for now + continue + a.sent = info + a.text = info.text + info["annotationSet"].append(a) + if "Target" in a: + for tspan in a.Target: + if tspan in target_spans: + self._warn( + 'Duplicate target span "{}"'.format( + info.text[slice(*tspan)] + ), + tspan, + "in sentence", + info["ID"], + info.text, + ) + # this can happen in cases like "chemical and biological weapons" + # being annotated as "chemical weapons" and "biological weapons" + else: + target_spans.add(tspan) + info["targets"].append((a.Target, a.luName, a.frameName)) + + assert info["annotationSet"][0].status == "UNANN" + info["POS"] = info["annotationSet"][0].POS + info["POS_tagset"] = info["annotationSet"][0].POS_tagset + return info + + def _handle_fulltextannotationset_elt(self, elt, is_pos=False): + """Load information from the given 'annotationSet' element. Each + 'annotationSet' contains several "layer" elements.""" + + info = self._handle_luannotationset_elt(elt, is_pos=is_pos) + if not is_pos: + info["_type"] = "fulltext_annotationset" + if "cxnID" not in info: # ignoring construction annotations for now + info["LU"] = self.lu( + info.luID, + luName=info.luName, + frameID=info.frameID, + frameName=info.frameName, + ) + info["frame"] = info.LU.frame + return info + + def _handle_fulltextlayer_elt(self, elt): + """Load information from the given 'layer' element. Each + 'layer' contains several "label" elements.""" + info = self._load_xml_attributes(AttrDict(), elt) + info["_type"] = "layer" + info["label"] = [] + + for sub in elt: + if sub.tag.endswith("label"): + l = self._load_xml_attributes(AttrDict(), sub) + info["label"].append(l) + + return info + + def _handle_framelexunit_elt(self, elt): + """Load the lexical unit info from an xml element in a frame's xml file.""" + luinfo = AttrDict() + luinfo["_type"] = "lu" + luinfo = self._load_xml_attributes(luinfo, elt) + luinfo["definition"] = "" + luinfo["definitionMarkup"] = "" + luinfo["sentenceCount"] = PrettyDict() + luinfo["lexemes"] = PrettyList() # multiword LUs have multiple lexemes + luinfo["semTypes"] = PrettyList() # an LU can have multiple semtypes + + for sub in elt: + if sub.tag.endswith("definition"): + luinfo["definitionMarkup"] = sub.text + luinfo["definition"] = self._strip_tags(sub.text) + elif sub.tag.endswith("sentenceCount"): + luinfo["sentenceCount"] = self._load_xml_attributes(PrettyDict(), sub) + elif sub.tag.endswith("lexeme"): + lexemeinfo = self._load_xml_attributes(PrettyDict(), sub) + if not isinstance(lexemeinfo.name, str): + # some lexeme names are ints by default: e.g., + # thousand.num has lexeme with name="1000" + lexemeinfo.name = str(lexemeinfo.name) + luinfo["lexemes"].append(lexemeinfo) + elif sub.tag.endswith("semType"): + semtypeinfo = self._load_xml_attributes(PrettyDict(), sub) + luinfo["semTypes"].append(self.semtype(semtypeinfo.ID)) + + # sort lexemes by 'order' attribute + # otherwise, e.g., 'write down.v' may have lexemes in wrong order + luinfo["lexemes"].sort(key=lambda x: x.order) + + return luinfo + + def _handle_lexunit_elt(self, elt, ignorekeys): + """ + Load full info for a lexical unit from its xml file. + This should only be called when accessing corpus annotations + (which are not included in frame files). + """ + luinfo = self._load_xml_attributes(AttrDict(), elt) + luinfo["_type"] = "lu" + luinfo["definition"] = "" + luinfo["definitionMarkup"] = "" + luinfo["subCorpus"] = PrettyList() + luinfo["lexemes"] = PrettyList() # multiword LUs have multiple lexemes + luinfo["semTypes"] = PrettyList() # an LU can have multiple semtypes + for k in ignorekeys: + if k in luinfo: + del luinfo[k] + + for sub in elt: + if sub.tag.endswith("header"): + continue # not used + elif sub.tag.endswith("valences"): + continue # not used + elif sub.tag.endswith("definition") and "definition" not in ignorekeys: + luinfo["definitionMarkup"] = sub.text + luinfo["definition"] = self._strip_tags(sub.text) + elif sub.tag.endswith("subCorpus") and "subCorpus" not in ignorekeys: + sc = self._handle_lusubcorpus_elt(sub) + if sc is not None: + luinfo["subCorpus"].append(sc) + elif sub.tag.endswith("lexeme") and "lexeme" not in ignorekeys: + luinfo["lexemes"].append(self._load_xml_attributes(PrettyDict(), sub)) + elif sub.tag.endswith("semType") and "semType" not in ignorekeys: + semtypeinfo = self._load_xml_attributes(AttrDict(), sub) + luinfo["semTypes"].append(self.semtype(semtypeinfo.ID)) + + return luinfo + + def _handle_lusubcorpus_elt(self, elt): + """Load a subcorpus of a lexical unit from the given xml.""" + sc = AttrDict() + try: + sc["name"] = elt.get("name") + except AttributeError: + return None + sc["_type"] = "lusubcorpus" + sc["sentence"] = [] + + for sub in elt: + if sub.tag.endswith("sentence"): + s = self._handle_lusentence_elt(sub) + if s is not None: + sc["sentence"].append(s) + + return sc + + def _handle_lusentence_elt(self, elt): + """Load a sentence from a subcorpus of an LU from xml.""" + info = self._load_xml_attributes(AttrDict(), elt) + info["_type"] = "lusentence" + info["annotationSet"] = [] + info["_ascii"] = types.MethodType( + _annotation_ascii, info + ) # attach a method for this instance + for sub in elt: + if sub.tag.endswith("text"): + info["text"] = self._strip_tags(sub.text) + elif sub.tag.endswith("annotationSet"): + annset = self._handle_luannotationset_elt( + sub, is_pos=(len(info["annotationSet"]) == 0) + ) + if annset is not None: + assert annset.status == "UNANN" or "FE" in annset, annset + if annset.status != "UNANN": + info["frameAnnotation"] = annset + # copy layer info up to current level + for k in ( + "Target", + "FE", + "FE2", + "FE3", + "GF", + "PT", + "POS", + "POS_tagset", + "Other", + "Sent", + "Verb", + "Noun", + "Adj", + "Adv", + "Prep", + "Scon", + "Art", + ): + if k in annset: + info[k] = annset[k] + info["annotationSet"].append(annset) + annset["sent"] = info + annset["text"] = info.text + return info + + def _handle_luannotationset_elt(self, elt, is_pos=False): + """Load an annotation set from a sentence in an subcorpus of an LU""" + info = self._load_xml_attributes(AttrDict(), elt) + info["_type"] = "posannotationset" if is_pos else "luannotationset" + info["layer"] = [] + info["_ascii"] = types.MethodType( + _annotation_ascii, info + ) # attach a method for this instance + + if "cxnID" in info: # ignoring construction annotations for now. + return info + + for sub in elt: + if sub.tag.endswith("layer"): + l = self._handle_lulayer_elt(sub) + if l is not None: + overt = [] + ni = {} # null instantiations + + info["layer"].append(l) + for lbl in l.label: + if "start" in lbl: + thespan = (lbl.start, lbl.end + 1, lbl.name) + if l.name not in ( + "Sent", + "Other", + ): # 'Sent' and 'Other' layers sometimes contain accidental duplicate spans + assert thespan not in overt, (info.ID, l.name, thespan) + overt.append(thespan) + else: # null instantiation + if lbl.name in ni: + self._warn( + "FE with multiple NI entries:", + lbl.name, + ni[lbl.name], + lbl.itype, + ) + else: + ni[lbl.name] = lbl.itype + overt = sorted(overt) + + if l.name == "Target": + if not overt: + self._warn( + "Skipping empty Target layer in annotation set ID={}".format( + info.ID + ) + ) + continue + assert all(lblname == "Target" for i, j, lblname in overt) + if "Target" in info: + self._warn( + "Annotation set {} has multiple Target layers".format( + info.ID + ) + ) + else: + info["Target"] = [(i, j) for (i, j, _) in overt] + elif l.name == "FE": + if l.rank == 1: + assert "FE" not in info + info["FE"] = (overt, ni) + # assert False,info + else: + # sometimes there are 3 FE layers! e.g. Change_position_on_a_scale.fall.v + assert 2 <= l.rank <= 3, l.rank + k = "FE" + str(l.rank) + assert k not in info + info[k] = (overt, ni) + elif l.name in ("GF", "PT"): + assert l.rank == 1 + info[l.name] = overt + elif l.name in ("BNC", "PENN"): + assert l.rank == 1 + info["POS"] = overt + info["POS_tagset"] = l.name + else: + if is_pos: + if l.name not in ("NER", "WSL"): + self._warn( + "Unexpected layer in sentence annotationset:", + l.name, + ) + else: + if l.name not in ( + "Sent", + "Verb", + "Noun", + "Adj", + "Adv", + "Prep", + "Scon", + "Art", + "Other", + ): + self._warn( + "Unexpected layer in frame annotationset:", l.name + ) + info[l.name] = overt + if not is_pos and "cxnID" not in info: + if "Target" not in info: + self._warn(f"Missing target in annotation set ID={info.ID}") + assert "FE" in info + if "FE3" in info: + assert "FE2" in info + + return info + + def _handle_lulayer_elt(self, elt): + """Load a layer from an annotation set""" + layer = self._load_xml_attributes(AttrDict(), elt) + layer["_type"] = "lulayer" + layer["label"] = [] + + for sub in elt: + if sub.tag.endswith("label"): + l = self._load_xml_attributes(AttrDict(), sub) + if l is not None: + layer["label"].append(l) + return layer + + def _handle_fe_elt(self, elt): + feinfo = self._load_xml_attributes(AttrDict(), elt) + feinfo["_type"] = "fe" + feinfo["definition"] = "" + feinfo["definitionMarkup"] = "" + feinfo["semType"] = None + feinfo["requiresFE"] = None + feinfo["excludesFE"] = None + for sub in elt: + if sub.tag.endswith("definition"): + feinfo["definitionMarkup"] = sub.text + feinfo["definition"] = self._strip_tags(sub.text) + elif sub.tag.endswith("semType"): + stinfo = self._load_xml_attributes(AttrDict(), sub) + feinfo["semType"] = self.semtype(stinfo.ID) + elif sub.tag.endswith("requiresFE"): + feinfo["requiresFE"] = self._load_xml_attributes(AttrDict(), sub) + elif sub.tag.endswith("excludesFE"): + feinfo["excludesFE"] = self._load_xml_attributes(AttrDict(), sub) + + return feinfo + + def _handle_semtype_elt(self, elt, tagspec=None): + semt = self._load_xml_attributes(AttrDict(), elt) + semt["_type"] = "semtype" + semt["superType"] = None + semt["subTypes"] = PrettyList() + for sub in elt: + if sub.text is not None: + semt["definitionMarkup"] = sub.text + semt["definition"] = self._strip_tags(sub.text) + else: + supertypeinfo = self._load_xml_attributes(AttrDict(), sub) + semt["superType"] = supertypeinfo + # the supertype may not have been loaded yet + + return semt + + +# +# Demo +# +def demo(): + from nltk.corpus import framenet as fn + + # + # It is not necessary to explicitly build the indexes by calling + # buildindexes(). We do this here just for demo purposes. If the + # indexes are not built explicitly, they will be built as needed. + # + print("Building the indexes...") + fn.buildindexes() + + # + # Get some statistics about the corpus + # + print("Number of Frames:", len(fn.frames())) + print("Number of Lexical Units:", len(fn.lus())) + print("Number of annotated documents:", len(fn.docs())) + print() + + # + # Frames + # + print( + 'getting frames whose name matches the (case insensitive) regex: "(?i)medical"' + ) + medframes = fn.frames(r"(?i)medical") + print(f'Found {len(medframes)} Frames whose name matches "(?i)medical":') + print([(f.name, f.ID) for f in medframes]) + + # + # store the first frame in the list of frames + # + tmp_id = medframes[0].ID + m_frame = fn.frame(tmp_id) # reads all info for the frame + + # + # get the frame relations + # + print( + '\nNumber of frame relations for the "{}" ({}) frame:'.format( + m_frame.name, m_frame.ID + ), + len(m_frame.frameRelations), + ) + for fr in m_frame.frameRelations: + print(" ", fr) + + # + # get the names of the Frame Elements + # + print( + f'\nNumber of Frame Elements in the "{m_frame.name}" frame:', + len(m_frame.FE), + ) + print(" ", [x for x in m_frame.FE]) + + # + # get the names of the "Core" Frame Elements + # + print(f'\nThe "core" Frame Elements in the "{m_frame.name}" frame:') + print(" ", [x.name for x in m_frame.FE.values() if x.coreType == "Core"]) + + # + # get all of the Lexical Units that are incorporated in the + # 'Ailment' FE of the 'Medical_conditions' frame (id=239) + # + print('\nAll Lexical Units that are incorporated in the "Ailment" FE:') + m_frame = fn.frame(239) + ailment_lus = [ + x + for x in m_frame.lexUnit.values() + if "incorporatedFE" in x and x.incorporatedFE == "Ailment" + ] + print(" ", [x.name for x in ailment_lus]) + + # + # get all of the Lexical Units for the frame + # + print( + f'\nNumber of Lexical Units in the "{m_frame.name}" frame:', + len(m_frame.lexUnit), + ) + print(" ", [x.name for x in m_frame.lexUnit.values()][:5], "...") + + # + # get basic info on the second LU in the frame + # + tmp_id = m_frame.lexUnit["ailment.n"].ID # grab the id of the specified LU + luinfo = fn.lu_basic(tmp_id) # get basic info on the LU + print(f"\nInformation on the LU: {luinfo.name}") + pprint(luinfo) + + # + # Get a list of all of the corpora used for fulltext annotation + # + print("\nNames of all of the corpora used for fulltext annotation:") + allcorpora = {x.corpname for x in fn.docs_metadata()} + pprint(list(allcorpora)) + + # + # Get the names of the annotated documents in the first corpus + # + firstcorp = list(allcorpora)[0] + firstcorp_docs = fn.docs(firstcorp) + print(f'\nNames of the annotated documents in the "{firstcorp}" corpus:') + pprint([x.filename for x in firstcorp_docs]) + + # + # Search for frames containing LUs whose name attribute matches a + # regexp pattern. + # + # Note: if you were going to be doing a lot of this type of + # searching, you'd want to build an index that maps from + # lemmas to frames because each time frames_by_lemma() is + # called, it has to search through ALL of the frame XML files + # in the db. + print( + '\nSearching for all Frames that have a lemma that matches the regexp: "^run.v$":' + ) + pprint(fn.frames_by_lemma(r"^run.v$")) + + +if __name__ == "__main__": + demo() diff --git a/nltk/corpus/reader/ieer.py b/nltk/corpus/reader/ieer.py new file mode 100644 index 0000000..3a9ab37 --- /dev/null +++ b/nltk/corpus/reader/ieer.py @@ -0,0 +1,119 @@ +# Natural Language Toolkit: IEER Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Corpus reader for the Information Extraction and Entity Recognition Corpus. + +NIST 1999 Information Extraction: Entity Recognition Evaluation +https://www.itl.nist.gov/iad/894.01/tests/ie-er/er_99/er_99.htm + +This corpus contains the NEWSWIRE development test data for the +NIST 1999 IE-ER Evaluation. The files were taken from the +subdirectory: ``/ie_er_99/english/devtest/newswire/*.ref.nwt`` +and filenames were shortened. + +The corpus contains the following files: APW_19980314, APW_19980424, +APW_19980429, NYT_19980315, NYT_19980403, and NYT_19980407. +""" + +import nltk +from nltk.corpus.reader.api import CorpusReader, StreamBackedCorpusView, concat + +#: A dictionary whose keys are the names of documents in this corpus; +#: and whose values are descriptions of those documents' contents. +titles = { + "APW_19980314": "Associated Press Weekly, 14 March 1998", + "APW_19980424": "Associated Press Weekly, 24 April 1998", + "APW_19980429": "Associated Press Weekly, 29 April 1998", + "NYT_19980315": "New York Times, 15 March 1998", + "NYT_19980403": "New York Times, 3 April 1998", + "NYT_19980407": "New York Times, 7 April 1998", +} + +#: A list of all documents in this corpus. +documents = sorted(titles) + + +class IEERDocument: + """ + A class to represent a single document from the IEER corpus. + Attributes include the document text, document number, document type, + date/time, and headline. + """ + + def __init__(self, text, docno=None, doctype=None, date_time=None, headline=""): + self.text = text + self.docno = docno + self.doctype = doctype + self.date_time = date_time + self.headline = headline + + def __repr__(self): + if self.headline: + headline = " ".join(self.headline.leaves()) + else: + headline = ( + " ".join([w for w in self.text.leaves() if w[:1] != "<"][:12]) + "..." + ) + if self.docno is not None: + return f"" + else: + return "" % headline + + +class IEERCorpusReader(CorpusReader): + """ + Corpus reader for the Information Extraction and Entity Recognition (IEER) Corpus. + """ + + def docs(self, fileids=None): + return concat( + [ + StreamBackedCorpusView(fileid, self._read_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def parsed_docs(self, fileids=None): + return concat( + [ + StreamBackedCorpusView(fileid, self._read_parsed_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def _read_parsed_block(self, stream): + return [self._parse(doc) for doc in self._read_block(stream)] + + def _parse(self, doc): + val = nltk.chunk.ieerstr2tree(doc, root_label="DOCUMENT") + if isinstance(val, dict): + return IEERDocument(**val) + else: + return IEERDocument(val) + + def _read_block(self, stream): + out = [] + # Skip any preamble. + while True: + line = stream.readline() + if not line: + return [] + if line.strip() == "": + break + out.append(line) + # Read the document + while True: + line = stream.readline() + if not line: + break + out.append(line) + if line.strip() == "": + break + # Return the document + return ["\n".join(out)] diff --git a/nltk/corpus/reader/indian.py b/nltk/corpus/reader/indian.py new file mode 100644 index 0000000..8f2e72e --- /dev/null +++ b/nltk/corpus/reader/indian.py @@ -0,0 +1,93 @@ +# Natural Language Toolkit: Indian Language POS-Tagged Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Indian Language POS-Tagged Corpus +Collected by A Kumaran, Microsoft Research, India +Distributed with permission + +Contents: + - Bangla: IIT Kharagpur + - Hindi: Microsoft Research India + - Marathi: IIT Bombay + - Telugu: IIIT Hyderabad +""" + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tag import map_tag, str2tuple + + +class IndianCorpusReader(CorpusReader): + """ + List of words, one per line. Blank lines are ignored. + """ + + def words(self, fileids=None): + return concat( + [ + IndianCorpusView(fileid, enc, False, False) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_words(self, fileids=None, tagset=None): + if tagset and tagset != self._tagset: + tag_mapping_function = lambda t: map_tag(self._tagset, tagset, t) + else: + tag_mapping_function = None + return concat( + [ + IndianCorpusView(fileid, enc, True, False, tag_mapping_function) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + return concat( + [ + IndianCorpusView(fileid, enc, False, True) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_sents(self, fileids=None, tagset=None): + if tagset and tagset != self._tagset: + tag_mapping_function = lambda t: map_tag(self._tagset, tagset, t) + else: + tag_mapping_function = None + return concat( + [ + IndianCorpusView(fileid, enc, True, True, tag_mapping_function) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + +class IndianCorpusView(StreamBackedCorpusView): + def __init__( + self, corpus_file, encoding, tagged, group_by_sent, tag_mapping_function=None + ): + self._tagged = tagged + self._group_by_sent = group_by_sent + self._tag_mapping_function = tag_mapping_function + StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) + + def read_block(self, stream): + line = stream.readline() + if line.startswith("<"): + return [] + sent = [str2tuple(word, sep="_") for word in line.split()] + if self._tag_mapping_function: + sent = [(w, self._tag_mapping_function(t)) for (w, t) in sent] + if not self._tagged: + sent = [w for (w, t) in sent] + if self._group_by_sent: + return [sent] + else: + return sent diff --git a/nltk/corpus/reader/ipipan.py b/nltk/corpus/reader/ipipan.py new file mode 100644 index 0000000..befe794 --- /dev/null +++ b/nltk/corpus/reader/ipipan.py @@ -0,0 +1,354 @@ +# Natural Language Toolkit: IPI PAN Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Konrad Goluchowski +# URL: +# For license information, see LICENSE.TXT + +import functools + +from nltk.corpus.reader.api import CorpusReader +from nltk.corpus.reader.util import StreamBackedCorpusView, concat + + +def _parse_args(fun): + @functools.wraps(fun) + def decorator(self, fileids=None, **kwargs): + kwargs.pop("tags", None) + if not fileids: + fileids = self.fileids() + return fun(self, fileids, **kwargs) + + return decorator + + +class IPIPANCorpusReader(CorpusReader): + """ + Corpus reader designed to work with corpus created by IPI PAN. + See http://korpus.pl/en/ for more details about IPI PAN corpus. + + The corpus includes information about text domain, channel and categories. + You can access possible values using ``domains()``, ``channels()`` and + ``categories()``. You can use also this metadata to filter files, e.g.: + ``fileids(channel='prasa')``, ``fileids(categories='publicystyczny')``. + + The reader supports methods: words, sents, paras and their tagged versions. + You can get part of speech instead of full tag by giving "simplify_tags=True" + parameter, e.g.: ``tagged_sents(simplify_tags=True)``. + + Also you can get all tags disambiguated tags specifying parameter + "one_tag=False", e.g.: ``tagged_paras(one_tag=False)``. + + You can get all tags that were assigned by a morphological analyzer specifying + parameter "disamb_only=False", e.g. ``tagged_words(disamb_only=False)``. + + The IPIPAN Corpus contains tags indicating if there is a space between two + tokens. To add special "no space" markers, you should specify parameter + "append_no_space=True", e.g. ``tagged_words(append_no_space=True)``. + As a result in place where there should be no space between two tokens new + pair ('', 'no-space') will be inserted (for tagged data) and just '' for + methods without tags. + + The corpus reader can also try to append spaces between words. To enable this + option, specify parameter "append_space=True", e.g. ``words(append_space=True)``. + As a result either ' ' or (' ', 'space') will be inserted between tokens. + + By default, xml entities like " and & are replaced by corresponding + characters. You can turn off this feature, specifying parameter + "replace_xmlentities=False", e.g. ``words(replace_xmlentities=False)``. + """ + + def __init__(self, root, fileids): + CorpusReader.__init__(self, root, fileids, None, None) + + def channels(self, fileids=None): + if not fileids: + fileids = self.fileids() + return self._parse_header(fileids, "channel") + + def domains(self, fileids=None): + if not fileids: + fileids = self.fileids() + return self._parse_header(fileids, "domain") + + def categories(self, fileids=None): + if not fileids: + fileids = self.fileids() + return [ + self._map_category(cat) for cat in self._parse_header(fileids, "keyTerm") + ] + + def fileids(self, channels=None, domains=None, categories=None): + if channels is not None and domains is not None and categories is not None: + raise ValueError( + "You can specify only one of channels, domains " + "and categories parameter at once" + ) + if channels is None and domains is None and categories is None: + return CorpusReader.fileids(self) + if isinstance(channels, str): + channels = [channels] + if isinstance(domains, str): + domains = [domains] + if isinstance(categories, str): + categories = [categories] + if channels: + return self._list_morph_files_by("channel", channels) + elif domains: + return self._list_morph_files_by("domain", domains) + else: + return self._list_morph_files_by( + "keyTerm", categories, map=self._map_category + ) + + @_parse_args + def sents(self, fileids=None, **kwargs): + return concat( + [ + self._view( + fileid, mode=IPIPANCorpusView.SENTS_MODE, tags=False, **kwargs + ) + for fileid in self._list_morph_files(fileids) + ] + ) + + @_parse_args + def paras(self, fileids=None, **kwargs): + return concat( + [ + self._view( + fileid, mode=IPIPANCorpusView.PARAS_MODE, tags=False, **kwargs + ) + for fileid in self._list_morph_files(fileids) + ] + ) + + @_parse_args + def words(self, fileids=None, **kwargs): + return concat( + [ + self._view(fileid, tags=False, **kwargs) + for fileid in self._list_morph_files(fileids) + ] + ) + + @_parse_args + def tagged_sents(self, fileids=None, **kwargs): + return concat( + [ + self._view(fileid, mode=IPIPANCorpusView.SENTS_MODE, **kwargs) + for fileid in self._list_morph_files(fileids) + ] + ) + + @_parse_args + def tagged_paras(self, fileids=None, **kwargs): + return concat( + [ + self._view(fileid, mode=IPIPANCorpusView.PARAS_MODE, **kwargs) + for fileid in self._list_morph_files(fileids) + ] + ) + + @_parse_args + def tagged_words(self, fileids=None, **kwargs): + return concat( + [self._view(fileid, **kwargs) for fileid in self._list_morph_files(fileids)] + ) + + def _list_morph_files(self, fileids): + return [f for f in self.abspaths(fileids)] + + def _list_header_files(self, fileids): + return [ + f.replace("morph.xml", "header.xml") + for f in self._list_morph_files(fileids) + ] + + def _parse_header(self, fileids, tag): + values = set() + for f in self._list_header_files(fileids): + values_list = self._get_tag(f, tag) + for v in values_list: + values.add(v) + return list(values) + + def _list_morph_files_by(self, tag, values, map=None): + fileids = self.fileids() + ret_fileids = set() + for f in fileids: + fp = self.abspath(f).replace("morph.xml", "header.xml") + values_list = self._get_tag(fp, tag) + for value in values_list: + if map is not None: + value = map(value) + if value in values: + ret_fileids.add(f) + return list(ret_fileids) + + def _get_tag(self, f, tag): + tags = [] + with open(f) as infile: + header = infile.read() + tag_end = 0 + while True: + tag_pos = header.find("<" + tag, tag_end) + if tag_pos < 0: + return tags + tag_end = header.find("", tag_pos) + tags.append(header[tag_pos + len(tag) + 2 : tag_end]) + + def _map_category(self, cat): + pos = cat.find(">") + if pos == -1: + return cat + else: + return cat[pos + 1 :] + + def _view(self, filename, **kwargs): + tags = kwargs.pop("tags", True) + mode = kwargs.pop("mode", 0) + simplify_tags = kwargs.pop("simplify_tags", False) + one_tag = kwargs.pop("one_tag", True) + disamb_only = kwargs.pop("disamb_only", True) + append_no_space = kwargs.pop("append_no_space", False) + append_space = kwargs.pop("append_space", False) + replace_xmlentities = kwargs.pop("replace_xmlentities", True) + + if len(kwargs) > 0: + raise ValueError("Unexpected arguments: %s" % kwargs.keys()) + if not one_tag and not disamb_only: + raise ValueError( + "You cannot specify both one_tag=False and " "disamb_only=False" + ) + if not tags and (simplify_tags or not one_tag or not disamb_only): + raise ValueError( + "You cannot specify simplify_tags, one_tag or " + "disamb_only with functions other than tagged_*" + ) + + return IPIPANCorpusView( + filename, + tags=tags, + mode=mode, + simplify_tags=simplify_tags, + one_tag=one_tag, + disamb_only=disamb_only, + append_no_space=append_no_space, + append_space=append_space, + replace_xmlentities=replace_xmlentities, + ) + + +class IPIPANCorpusView(StreamBackedCorpusView): + WORDS_MODE = 0 + SENTS_MODE = 1 + PARAS_MODE = 2 + + def __init__(self, filename, startpos=0, **kwargs): + StreamBackedCorpusView.__init__(self, filename, None, startpos, None) + self.in_sentence = False + self.position = 0 + + self.show_tags = kwargs.pop("tags", True) + self.disamb_only = kwargs.pop("disamb_only", True) + self.mode = kwargs.pop("mode", IPIPANCorpusView.WORDS_MODE) + self.simplify_tags = kwargs.pop("simplify_tags", False) + self.one_tag = kwargs.pop("one_tag", True) + self.append_no_space = kwargs.pop("append_no_space", False) + self.append_space = kwargs.pop("append_space", False) + self.replace_xmlentities = kwargs.pop("replace_xmlentities", True) + + def read_block(self, stream): + sentence = [] + sentences = [] + space = False + no_space = False + + tags = set() + + lines = self._read_data(stream) + + while True: + # we may have only part of last line + if len(lines) <= 1: + self._seek(stream) + lines = self._read_data(stream) + + if lines == [""]: + assert not sentences + return [] + + line = lines.pop() + self.position += len(line) + 1 + + if line.startswith('"): + if self.append_space: + no_space = True + if self.append_no_space: + if self.show_tags: + sentence.append(("", "no-space")) + else: + sentence.append("") + elif line.startswith(" +# URL: +# For license information, see LICENSE.TXT + +# For more information, see http://lilyx.net/pages/nltkjapanesecorpus.html + +import re + +from nltk.corpus.reader.api import CorpusReader, SyntaxCorpusReader +from nltk.corpus.reader.util import ( + FileSystemPathPointer, + find_corpus_fileids, + read_blankline_block, +) +from nltk.parse import DependencyGraph + +# default function to convert morphlist to str for tree representation +_morphs2str_default = lambda morphs: "/".join(m[0] for m in morphs if m[0] != "EOS") + + +class KNBCorpusReader(SyntaxCorpusReader): + """ + This class implements: + - ``__init__``, which specifies the location of the corpus + and a method for detecting the sentence blocks in corpus files. + - ``_read_block``, which reads a block from the input stream. + - ``_word``, which takes a block and returns a list of list of words. + - ``_tag``, which takes a block and returns a list of list of tagged + words. + - ``_parse``, which takes a block and returns a list of parsed + sentences. + + The structure of tagged words: + tagged_word = (word(str), tags(tuple)) + tags = (surface, reading, lemma, pos1, posid1, pos2, posid2, pos3, posid3, others ...) + + Usage example + + >>> from nltk.corpus.util import LazyCorpusLoader + >>> knbc = LazyCorpusLoader( + ... 'knbc/corpus1', + ... KNBCorpusReader, + ... r'.*/KN.*', + ... encoding='euc-jp', + ... ) + + >>> len(knbc.sents()[0]) + 9 + + """ + + def __init__(self, root, fileids, encoding="utf8", morphs2str=_morphs2str_default): + """ + Initialize KNBCorpusReader + morphs2str is a function to convert morphlist to str for tree representation + for _parse() + """ + SyntaxCorpusReader.__init__(self, root, fileids, encoding) + self.morphs2str = morphs2str + + def _read_block(self, stream): + # blocks are split by blankline (or EOF) - default + return read_blankline_block(stream) + + def _word(self, t): + res = [] + for line in t.splitlines(): + # ignore the Bunsets headers + if not re.match(r"EOS|\*|\#|\+", line): + cells = line.strip().split(" ") + res.append(cells[0]) + + return res + + # ignores tagset argument + def _tag(self, t, tagset=None): + res = [] + for line in t.splitlines(): + # ignore the Bunsets headers + if not re.match(r"EOS|\*|\#|\+", line): + cells = line.strip().split(" ") + # convert cells to morph tuples + res.append((cells[0], " ".join(cells[1:]))) + + return res + + def _parse(self, t): + dg = DependencyGraph() + i = 0 + for line in t.splitlines(): + if line[0] in "*+": + # start of bunsetsu or tag + + cells = line.strip().split(" ", 3) + m = re.match(r"([\-0-9]*)([ADIP])", cells[1]) + + assert m is not None + + node = dg.nodes[i] + node.update({"address": i, "rel": m.group(2), "word": []}) + + dep_parent = int(m.group(1)) + + if dep_parent == -1: + dg.root = node + else: + dg.nodes[dep_parent]["deps"].append(i) + + i += 1 + elif line[0] != "#": + # normal morph + cells = line.strip().split(" ") + # convert cells to morph tuples + morph = cells[0], " ".join(cells[1:]) + dg.nodes[i - 1]["word"].append(morph) + + if self.morphs2str: + for node in dg.nodes.values(): + node["word"] = self.morphs2str(node["word"]) + + return dg.tree() + + +###################################################################### +# Demo +###################################################################### + + +def demo(): + import nltk + from nltk.corpus.util import LazyCorpusLoader + + root = nltk.data.find("corpora/knbc/corpus1") + fileids = [ + f + for f in find_corpus_fileids(FileSystemPathPointer(root), ".*") + if re.search(r"\d\-\d\-[\d]+\-[\d]+", f) + ] + + def _knbc_fileids_sort(x): + cells = x.split("-") + return (cells[0], int(cells[1]), int(cells[2]), int(cells[3])) + + knbc = LazyCorpusLoader( + "knbc/corpus1", + KNBCorpusReader, + sorted(fileids, key=_knbc_fileids_sort), + encoding="euc-jp", + ) + + print(knbc.fileids()[:10]) + print("".join(knbc.words()[:100])) + + print("\n\n".join(str(tree) for tree in knbc.parsed_sents()[:2])) + + knbc.morphs2str = lambda morphs: "/".join( + "{}({})".format(m[0], m[1].split(" ")[2]) for m in morphs if m[0] != "EOS" + ).encode("utf-8") + + print("\n\n".join("%s" % tree for tree in knbc.parsed_sents()[:2])) + + print( + "\n".join( + " ".join("{}/{}".format(w[0], w[1].split(" ")[2]) for w in sent) + for sent in knbc.tagged_sents()[0:2] + ) + ) + + +def test(): + from nltk.corpus.util import LazyCorpusLoader + + knbc = LazyCorpusLoader( + "knbc/corpus1", KNBCorpusReader, r".*/KN.*", encoding="euc-jp" + ) + assert isinstance(knbc.words()[0], str) + assert isinstance(knbc.sents()[0][0], str) + assert isinstance(knbc.tagged_words()[0], tuple) + assert isinstance(knbc.tagged_sents()[0][0], tuple) + + +if __name__ == "__main__": + demo() diff --git a/nltk/corpus/reader/lin.py b/nltk/corpus/reader/lin.py new file mode 100644 index 0000000..971b218 --- /dev/null +++ b/nltk/corpus/reader/lin.py @@ -0,0 +1,183 @@ +# Natural Language Toolkit: Lin's Thesaurus +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Dan Blanchard +# URL: +# For license information, see LICENSE.txt + +import re +from collections import defaultdict +from functools import reduce + +from nltk.corpus.reader import CorpusReader + + +class LinThesaurusCorpusReader(CorpusReader): + """Wrapper for the LISP-formatted thesauruses distributed by Dekang Lin.""" + + # Compiled regular expression for extracting the key from the first line of each + # thesaurus entry + _key_re = re.compile(r'\("?([^"]+)"? \(desc [0-9.]+\).+') + + @staticmethod + def __defaultdict_factory(): + """Factory for creating defaultdict of defaultdict(dict)s""" + return defaultdict(dict) + + def __init__(self, root, badscore=0.0): + """ + Initialize the thesaurus. + + :param root: root directory containing thesaurus LISP files + :type root: C{string} + :param badscore: the score to give to words which do not appear in each other's sets of synonyms + :type badscore: C{float} + """ + + super().__init__(root, r"sim[A-Z]\.lsp") + self._thesaurus = defaultdict(LinThesaurusCorpusReader.__defaultdict_factory) + self._badscore = badscore + for path, encoding, fileid in self.abspaths( + include_encoding=True, include_fileid=True + ): + with open(path) as lin_file: + first = True + for line in lin_file: + line = line.strip() + # Start of entry + if first: + key = LinThesaurusCorpusReader._key_re.sub(r"\1", line) + first = False + # End of entry + elif line == "))": + first = True + # Lines with pairs of ngrams and scores + else: + split_line = line.split("\t") + if len(split_line) == 2: + ngram, score = split_line + self._thesaurus[fileid][key][ngram.strip('"')] = float( + score + ) + + def similarity(self, ngram1, ngram2, fileid=None): + """ + Returns the similarity score for two ngrams. + + :param ngram1: first ngram to compare + :type ngram1: C{string} + :param ngram2: second ngram to compare + :type ngram2: C{string} + :param fileid: thesaurus fileid to search in. If None, search all fileids. + :type fileid: C{string} + :return: If fileid is specified, just the score for the two ngrams; otherwise, + list of tuples of fileids and scores. + """ + # Entries don't contain themselves, so make sure similarity between item and itself is 1.0 + if ngram1 == ngram2: + if fileid: + return 1.0 + else: + return [(fid, 1.0) for fid in self._fileids] + else: + if fileid: + return ( + self._thesaurus[fileid][ngram1][ngram2] + if ngram2 in self._thesaurus[fileid][ngram1] + else self._badscore + ) + else: + return [ + ( + fid, + ( + self._thesaurus[fid][ngram1][ngram2] + if ngram2 in self._thesaurus[fid][ngram1] + else self._badscore + ), + ) + for fid in self._fileids + ] + + def scored_synonyms(self, ngram, fileid=None): + """ + Returns a list of scored synonyms (tuples of synonyms and scores) for the current ngram + + :param ngram: ngram to lookup + :type ngram: C{string} + :param fileid: thesaurus fileid to search in. If None, search all fileids. + :type fileid: C{string} + :return: If fileid is specified, list of tuples of scores and synonyms; otherwise, + list of tuples of fileids and lists, where inner lists consist of tuples of + scores and synonyms. + """ + if fileid: + return self._thesaurus[fileid][ngram].items() + else: + return [ + (fileid, self._thesaurus[fileid][ngram].items()) + for fileid in self._fileids + ] + + def synonyms(self, ngram, fileid=None): + """ + Returns a list of synonyms for the current ngram. + + :param ngram: ngram to lookup + :type ngram: C{string} + :param fileid: thesaurus fileid to search in. If None, search all fileids. + :type fileid: C{string} + :return: If fileid is specified, list of synonyms; otherwise, list of tuples of fileids and + lists, where inner lists contain synonyms. + """ + if fileid: + return self._thesaurus[fileid][ngram].keys() + else: + return [ + (fileid, self._thesaurus[fileid][ngram].keys()) + for fileid in self._fileids + ] + + def __contains__(self, ngram): + """ + Determines whether or not the given ngram is in the thesaurus. + + :param ngram: ngram to lookup + :type ngram: C{string} + :return: whether the given ngram is in the thesaurus. + """ + return reduce( + lambda accum, fileid: accum or (ngram in self._thesaurus[fileid]), + self._fileids, + False, + ) + + +###################################################################### +# Demo +###################################################################### + + +def demo(): + from nltk.corpus import lin_thesaurus as thes + + word1 = "business" + word2 = "enterprise" + print("Getting synonyms for " + word1) + print(thes.synonyms(word1)) + + print("Getting scored synonyms for " + word1) + print(thes.scored_synonyms(word1)) + + print("Getting synonyms from simN.lsp (noun subsection) for " + word1) + print(thes.synonyms(word1, fileid="simN.lsp")) + + print("Getting synonyms from simN.lsp (noun subsection) for " + word1) + print(thes.synonyms(word1, fileid="simN.lsp")) + + print(f"Similarity score for {word1} and {word2}:") + print(thes.similarity(word1, word2)) + + +if __name__ == "__main__": + demo() diff --git a/nltk/corpus/reader/markdown.py b/nltk/corpus/reader/markdown.py new file mode 100644 index 0000000..a49edc4 --- /dev/null +++ b/nltk/corpus/reader/markdown.py @@ -0,0 +1,353 @@ +from collections import namedtuple +from functools import partial, wraps + +from nltk.corpus.reader.api import CategorizedCorpusReader +from nltk.corpus.reader.plaintext import PlaintextCorpusReader +from nltk.corpus.reader.util import concat, read_blankline_block +from nltk.tokenize import blankline_tokenize, sent_tokenize, word_tokenize + + +def comma_separated_string_args(func): + """ + A decorator that allows a function to be called with + a single string of comma-separated values which become + individual function arguments. + """ + + @wraps(func) + def wrapper(*args, **kwargs): + _args = list() + for arg in args: + if isinstance(arg, str): + _args.append({part.strip() for part in arg.split(",")}) + elif isinstance(arg, list): + _args.append(set(arg)) + else: + _args.append(arg) + for name, value in kwargs.items(): + if isinstance(value, str): + kwargs[name] = {part.strip() for part in value.split(",")} + return func(*_args, **kwargs) + + return wrapper + + +def read_parse_blankline_block(stream, parser): + block = read_blankline_block(stream) + if block: + return [parser.render(block[0])] + return block + + +class MarkdownBlock: + def __init__(self, content): + self.content = content + self.truncate_at = 16 + + def __repr__(self): + return f"{self.__class__.__name__}(content={repr(str(self))})" + + def __str__(self): + return ( + f"{self.content[:self.truncate_at]}" + f"{'...' if len(self.content) > self.truncate_at else ''}" + ) + + @property + def raw(self): + return self.content + + @property + def words(self): + return word_tokenize(self.content) + + @property + def sents(self): + return [word_tokenize(sent) for sent in sent_tokenize(self.content)] + + @property + def paras(self): + return [ + [word_tokenize(sent) for sent in sent_tokenize(para)] + for para in blankline_tokenize(self.content) + ] + + +class CodeBlock(MarkdownBlock): + def __init__(self, language, *args): + self.language = language + super().__init__(*args) + + @property + def sents(self): + return [word_tokenize(line) for line in self.content.splitlines()] + + @property + def lines(self): + return self.content.splitlines() + + @property + def paras(self): + return [ + [word_tokenize(line) for line in para.splitlines()] + for para in blankline_tokenize(self.content) + ] + + +class MarkdownSection(MarkdownBlock): + def __init__(self, heading, level, *args): + self.heading = heading + self.level = level + super().__init__(*args) + + +Image = namedtuple("Image", "label, src, title") +Link = namedtuple("Link", "label, href, title") +List = namedtuple("List", "is_ordered, items") + + +class MarkdownCorpusReader(PlaintextCorpusReader): + def __init__(self, *args, parser=None, **kwargs): + from markdown_it import MarkdownIt + from mdit_plain.renderer import RendererPlain + from mdit_py_plugins.front_matter import front_matter_plugin + + self.parser = parser + if self.parser is None: + self.parser = MarkdownIt("commonmark", renderer_cls=RendererPlain) + self.parser.use(front_matter_plugin) + + kwargs.setdefault( + "para_block_reader", partial(read_parse_blankline_block, parser=self.parser) + ) + super().__init__(*args, **kwargs) + + # This override takes care of removing markup. + def _read_word_block(self, stream): + words = list() + for para in self._para_block_reader(stream): + words.extend(self._word_tokenizer.tokenize(para)) + return words + + +class CategorizedMarkdownCorpusReader(CategorizedCorpusReader, MarkdownCorpusReader): + """ + A reader for markdown corpora whose documents are divided into + categories based on their file identifiers. + + Based on nltk.corpus.reader.plaintext.CategorizedPlaintextCorpusReader: + https://www.nltk.org/_modules/nltk/corpus/reader/api.html#CategorizedCorpusReader + """ + + def __init__(self, *args, cat_field="tags", **kwargs): + """ + Initialize the corpus reader. Categorization arguments + (``cat_pattern``, ``cat_map``, and ``cat_file``) are passed to + the ``CategorizedCorpusReader`` constructor. The remaining arguments + are passed to the ``MarkdownCorpusReader`` constructor. + """ + cat_args = ["cat_pattern", "cat_map", "cat_file"] + if not any(arg in kwargs for arg in cat_args): + # Initialize with a blank map now, + # and try to build categories from document metadata later. + kwargs["cat_map"] = dict() + CategorizedCorpusReader.__init__(self, kwargs) + MarkdownCorpusReader.__init__(self, *args, **kwargs) + + # Map file IDs to categories if self._map exists but is still empty: + if self._map is not None and not self._map: + for file_id in self._fileids: + metadata = self.metadata(file_id) + if metadata: + self._map[file_id] = metadata[0].get(cat_field, []) + + ### Begin CategorizedCorpusReader Overrides + @comma_separated_string_args + def categories(self, fileids=None): + return super().categories(fileids) + + @comma_separated_string_args + def fileids(self, categories=None): + if categories is None: + return self._fileids + return super().fileids(categories) + + ### End CategorizedCorpusReader Overrides + + ### Begin MarkdownCorpusReader Overrides + @comma_separated_string_args + def raw(self, fileids=None, categories=None): + return super().raw(self._resolve(fileids, categories)) + + @comma_separated_string_args + def words(self, fileids=None, categories=None): + return super().words(self._resolve(fileids, categories)) + + @comma_separated_string_args + def sents(self, fileids=None, categories=None): + return super().sents(self._resolve(fileids, categories)) + + @comma_separated_string_args + def paras(self, fileids=None, categories=None): + return super().paras(self._resolve(fileids, categories)) + + ### End MarkdownCorpusReader Overrides + + def concatenated_view(self, reader, fileids, categories): + return concat( + [ + self.CorpusView(path, reader, encoding=enc) + for (path, enc) in self.abspaths( + self._resolve(fileids, categories), include_encoding=True + ) + ] + ) + + def metadata_reader(self, stream): + from yaml import safe_load + + return [ + safe_load(t.content) + for t in self.parser.parse(stream.read()) + if t.type == "front_matter" + ] + + @comma_separated_string_args + def metadata(self, fileids=None, categories=None): + return self.concatenated_view(self.metadata_reader, fileids, categories) + + def blockquote_reader(self, stream): + tokens = self.parser.parse(stream.read()) + # Record the index of each top-level blockquote_open/blockquote_close + # with two linear scans and pair them positionally, instead of calling + # tokens.index() once per block: that is an O(n) scan per block, i.e. + # O(n^2) overall (CWE-407) on a document made of many top-level + # blockquotes. + opening_indices = [ + i + for i, t in enumerate(tokens) + if t.level == 0 and t.type == "blockquote_open" + ] + closing_indices = [ + i + for i, t in enumerate(tokens) + if t.level == 0 and t.type == "blockquote_close" + ] + blockquotes = [ + tokens[o : c + 1] for o, c in zip(opening_indices, closing_indices) + ] + return [ + MarkdownBlock( + self.parser.renderer.render(block, self.parser.options, env=None) + ) + for block in blockquotes + ] + + @comma_separated_string_args + def blockquotes(self, fileids=None, categories=None): + return self.concatenated_view(self.blockquote_reader, fileids, categories) + + def code_block_reader(self, stream): + return [ + CodeBlock( + t.info, + t.content, + ) + for t in self.parser.parse(stream.read()) + if t.level == 0 and t.type in ("fence", "code_block") + ] + + @comma_separated_string_args + def code_blocks(self, fileids=None, categories=None): + return self.concatenated_view(self.code_block_reader, fileids, categories) + + def image_reader(self, stream): + return [ + Image( + child_token.content, + child_token.attrGet("src"), + child_token.attrGet("title"), + ) + for inline_token in filter( + lambda t: t.type == "inline", self.parser.parse(stream.read()) + ) + for child_token in inline_token.children + if child_token.type == "image" + ] + + @comma_separated_string_args + def images(self, fileids=None, categories=None): + return self.concatenated_view(self.image_reader, fileids, categories) + + def link_reader(self, stream): + return [ + Link( + inline_token.children[i + 1].content, + child_token.attrGet("href"), + child_token.attrGet("title"), + ) + for inline_token in filter( + lambda t: t.type == "inline", self.parser.parse(stream.read()) + ) + for i, child_token in enumerate(inline_token.children) + if child_token.type == "link_open" + ] + + @comma_separated_string_args + def links(self, fileids=None, categories=None): + return self.concatenated_view(self.link_reader, fileids, categories) + + def list_reader(self, stream): + tokens = self.parser.parse(stream.read()) + opening_types = ("bullet_list_open", "ordered_list_open") + closing_types = ("bullet_list_close", "ordered_list_close") + # Pair each top-level list_open with its list_close by index, collected + # with two linear scans, instead of an O(n) tokens.index() scan per block + # which makes the loop O(n^2) (CWE-407) on a document of many top-level + # lists. + opening_indices = [ + i for i, t in enumerate(tokens) if t.level == 0 and t.type in opening_types + ] + closing_indices = [ + i for i, t in enumerate(tokens) if t.level == 0 and t.type in closing_types + ] + list_blocks = [ + tokens[o : c + 1] for o, c in zip(opening_indices, closing_indices) + ] + return [ + List( + block[0].type == "ordered_list_open", + [t.content for t in block if t.content], + ) + for block in list_blocks + ] + + @comma_separated_string_args + def lists(self, fileids=None, categories=None): + return self.concatenated_view(self.list_reader, fileids, categories) + + def section_reader(self, stream): + section_blocks, block = list(), list() + for t in self.parser.parse(stream.read()): + if t.level == 0 and t.type == "heading_open": + if not block: + block.append(t) + else: + section_blocks.append(block) + block = [t] + elif block: + block.append(t) + if block: + section_blocks.append(block) + return [ + MarkdownSection( + block[1].content, + block[0].markup.count("#"), + self.parser.renderer.render(block, self.parser.options, env=None), + ) + for block in section_blocks + ] + + @comma_separated_string_args + def sections(self, fileids=None, categories=None): + return self.concatenated_view(self.section_reader, fileids, categories) diff --git a/nltk/corpus/reader/mte.py b/nltk/corpus/reader/mte.py new file mode 100644 index 0000000..0f40e28 --- /dev/null +++ b/nltk/corpus/reader/mte.py @@ -0,0 +1,398 @@ +""" +A reader for corpora whose documents are in MTE format. +""" + +import os +import re +from functools import reduce + +from nltk.corpus.reader import TaggedCorpusReader, concat +from nltk.corpus.reader.xmldocs import XMLCorpusView + + +def xpath(root, path, ns): + return root.findall(path, ns) + + +class MTECorpusView(XMLCorpusView): + """ + Class for lazy viewing the MTE Corpus. + """ + + def __init__(self, fileid, tagspec, elt_handler=None): + XMLCorpusView.__init__(self, fileid, tagspec, elt_handler) + + def read_block(self, stream, tagspec=None, elt_handler=None): + return list( + filter( + lambda x: x is not None, + XMLCorpusView.read_block(self, stream, tagspec, elt_handler), + ) + ) + + +class MTEFileReader: + """ + Class for loading the content of the multext-east corpus. It + parses the xml files and does some tag-filtering depending on the + given method parameters. + """ + + ns = { + "tei": "https://www.tei-c.org/ns/1.0", + "xml": "https://www.w3.org/XML/1998/namespace", + } + tag_ns = "{https://www.tei-c.org/ns/1.0}" + xml_ns = "{https://www.w3.org/XML/1998/namespace}" + word_path = "TEI/text/body/div/div/p/s/(w|c)" + sent_path = "TEI/text/body/div/div/p/s" + para_path = "TEI/text/body/div/div/p" + + def __init__(self, file_path): + self.__file_path = file_path + + @classmethod + def _word_elt(cls, elt, context): + return elt.text + + @classmethod + def _sent_elt(cls, elt, context): + return [cls._word_elt(w, None) for w in xpath(elt, "*", cls.ns)] + + @classmethod + def _para_elt(cls, elt, context): + return [cls._sent_elt(s, None) for s in xpath(elt, "*", cls.ns)] + + @classmethod + def _tagged_word_elt(cls, elt, context): + if "ana" not in elt.attrib: + return (elt.text, "") + + if cls.__tags == "" and cls.__tagset == "msd": + return (elt.text, elt.attrib["ana"]) + elif cls.__tags == "" and cls.__tagset == "universal": + return (elt.text, MTETagConverter.msd_to_universal(elt.attrib["ana"])) + else: + tags = re.compile("^" + re.sub("-", ".", cls.__tags) + ".*$") + if tags.match(elt.attrib["ana"]): + if cls.__tagset == "msd": + return (elt.text, elt.attrib["ana"]) + else: + return ( + elt.text, + MTETagConverter.msd_to_universal(elt.attrib["ana"]), + ) + else: + return None + + @classmethod + def _tagged_sent_elt(cls, elt, context): + return list( + filter( + lambda x: x is not None, + [cls._tagged_word_elt(w, None) for w in xpath(elt, "*", cls.ns)], + ) + ) + + @classmethod + def _tagged_para_elt(cls, elt, context): + return list( + filter( + lambda x: x is not None, + [cls._tagged_sent_elt(s, None) for s in xpath(elt, "*", cls.ns)], + ) + ) + + @classmethod + def _lemma_word_elt(cls, elt, context): + if "lemma" not in elt.attrib: + return (elt.text, "") + else: + return (elt.text, elt.attrib["lemma"]) + + @classmethod + def _lemma_sent_elt(cls, elt, context): + return [cls._lemma_word_elt(w, None) for w in xpath(elt, "*", cls.ns)] + + @classmethod + def _lemma_para_elt(cls, elt, context): + return [cls._lemma_sent_elt(s, None) for s in xpath(elt, "*", cls.ns)] + + def words(self): + return MTECorpusView( + self.__file_path, MTEFileReader.word_path, MTEFileReader._word_elt + ) + + def sents(self): + return MTECorpusView( + self.__file_path, MTEFileReader.sent_path, MTEFileReader._sent_elt + ) + + def paras(self): + return MTECorpusView( + self.__file_path, MTEFileReader.para_path, MTEFileReader._para_elt + ) + + def lemma_words(self): + return MTECorpusView( + self.__file_path, MTEFileReader.word_path, MTEFileReader._lemma_word_elt + ) + + def tagged_words(self, tagset, tags): + MTEFileReader.__tagset = tagset + MTEFileReader.__tags = tags + return MTECorpusView( + self.__file_path, MTEFileReader.word_path, MTEFileReader._tagged_word_elt + ) + + def lemma_sents(self): + return MTECorpusView( + self.__file_path, MTEFileReader.sent_path, MTEFileReader._lemma_sent_elt + ) + + def tagged_sents(self, tagset, tags): + MTEFileReader.__tagset = tagset + MTEFileReader.__tags = tags + return MTECorpusView( + self.__file_path, MTEFileReader.sent_path, MTEFileReader._tagged_sent_elt + ) + + def lemma_paras(self): + return MTECorpusView( + self.__file_path, MTEFileReader.para_path, MTEFileReader._lemma_para_elt + ) + + def tagged_paras(self, tagset, tags): + MTEFileReader.__tagset = tagset + MTEFileReader.__tags = tags + return MTECorpusView( + self.__file_path, MTEFileReader.para_path, MTEFileReader._tagged_para_elt + ) + + +class MTETagConverter: + """ + Class for converting msd tags to universal tags, more conversion + options are currently not implemented. + """ + + mapping_msd_universal = { + "A": "ADJ", + "S": "ADP", + "R": "ADV", + "C": "CONJ", + "D": "DET", + "N": "NOUN", + "M": "NUM", + "Q": "PRT", + "P": "PRON", + "V": "VERB", + ".": ".", + "-": "X", + } + + @staticmethod + def msd_to_universal(tag): + """ + This function converts the annotation from the Multex-East to the universal tagset + as described in Chapter 5 of the NLTK-Book + + Unknown Tags will be mapped to X. Punctuation marks are not supported in MSD tags, so + """ + indicator = tag[0] if not tag[0] == "#" else tag[1] + + if indicator not in MTETagConverter.mapping_msd_universal: + indicator = "-" + + return MTETagConverter.mapping_msd_universal[indicator] + + +class MTECorpusReader(TaggedCorpusReader): + """ + Reader for corpora following the TEI-p5 xml scheme, such as MULTEXT-East. + MULTEXT-East contains part-of-speech-tagged words with a quite precise tagging + scheme. These tags can be converted to the Universal tagset + """ + + def __init__(self, root=None, fileids=None, encoding="utf8"): + """ + Construct a new MTECorpusreader for a set of documents + located at the given root directory. Example usage: + + >>> root = '/...path to corpus.../' + >>> reader = MTECorpusReader(root, 'oana-*.xml', 'utf8') # doctest: +SKIP + + :param root: The root directory for this corpus. (default points to location in multext config file) + :param fileids: A list or regexp specifying the fileids in this corpus. (default is oana-en.xml) + :param encoding: The encoding of the given files (default is utf8) + """ + TaggedCorpusReader.__init__(self, root, fileids, encoding) + self._readme = "00README.txt" + + def __fileids(self, fileids): + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + # filter wrong userinput + fileids = filter(lambda x: x in self._fileids, fileids) + # filter multext-east sourcefiles that are not compatible to the teip5 specification + fileids = filter(lambda x: x not in ["oana-bg.xml", "oana-mk.xml"], fileids) + if not fileids: + print("No valid multext-east file specified") + return fileids + + def words(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).words() + for f in self.__fileids(fileids) + ] + ) + + def sents(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a list of sentences or utterances, + each encoded as a list of word strings + :rtype: list(list(str)) + """ + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).sents() + for f in self.__fileids(fileids) + ] + ) + + def paras(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a list of paragraphs, each encoded as a list + of sentences, which are in turn encoded as lists of word string + :rtype: list(list(list(str))) + """ + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).paras() + for f in self.__fileids(fileids) + ] + ) + + def lemma_words(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a list of words, the corresponding lemmas + and punctuation symbols, encoded as tuples (word, lemma) + :rtype: list(tuple(str,str)) + """ + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).lemma_words() + for f in self.__fileids(fileids) + ] + ) + + def tagged_words(self, fileids=None, tagset="msd", tags=""): + """ + :param fileids: A list specifying the fileids that should be used. + :param tagset: The tagset that should be used in the returned object, + either "universal" or "msd", "msd" is the default + :param tags: An MSD Tag that is used to filter all parts of the used corpus + that are not more precise or at least equal to the given tag + :return: the given file(s) as a list of tagged words and punctuation symbols + encoded as tuples (word, tag) + :rtype: list(tuple(str, str)) + """ + if tagset == "universal" or tagset == "msd": + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).tagged_words( + tagset, tags + ) + for f in self.__fileids(fileids) + ] + ) + else: + print("Unknown tagset specified.") + + def lemma_sents(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a list of sentences or utterances, each + encoded as a list of tuples of the word and the corresponding + lemma (word, lemma) + :rtype: list(list(tuple(str, str))) + """ + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).lemma_sents() + for f in self.__fileids(fileids) + ] + ) + + def tagged_sents(self, fileids=None, tagset="msd", tags=""): + """ + :param fileids: A list specifying the fileids that should be used. + :param tagset: The tagset that should be used in the returned object, + either "universal" or "msd", "msd" is the default + :param tags: An MSD Tag that is used to filter all parts of the used corpus + that are not more precise or at least equal to the given tag + :return: the given file(s) as a list of sentences or utterances, each + each encoded as a list of (word,tag) tuples + :rtype: list(list(tuple(str, str))) + """ + if tagset == "universal" or tagset == "msd": + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).tagged_sents( + tagset, tags + ) + for f in self.__fileids(fileids) + ] + ) + else: + print("Unknown tagset specified.") + + def lemma_paras(self, fileids=None): + """ + :param fileids: A list specifying the fileids that should be used. + :return: the given file(s) as a list of paragraphs, each encoded as a + list of sentences, which are in turn encoded as a list of + tuples of the word and the corresponding lemma (word, lemma) + :rtype: list(List(List(tuple(str, str)))) + """ + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).lemma_paras() + for f in self.__fileids(fileids) + ] + ) + + def tagged_paras(self, fileids=None, tagset="msd", tags=""): + """ + :param fileids: A list specifying the fileids that should be used. + :param tagset: The tagset that should be used in the returned object, + either "universal" or "msd", "msd" is the default + :param tags: An MSD Tag that is used to filter all parts of the used corpus + that are not more precise or at least equal to the given tag + :return: the given file(s) as a list of paragraphs, each encoded as a + list of sentences, which are in turn encoded as a list + of (word,tag) tuples + :rtype: list(list(list(tuple(str, str)))) + """ + if tagset == "universal" or tagset == "msd": + return concat( + [ + MTEFileReader(os.path.join(str(self._root), f)).tagged_paras( + tagset, tags + ) + for f in self.__fileids(fileids) + ] + ) + else: + print("Unknown tagset specified.") diff --git a/nltk/corpus/reader/nkjp.py b/nltk/corpus/reader/nkjp.py new file mode 100644 index 0000000..4a567a9 --- /dev/null +++ b/nltk/corpus/reader/nkjp.py @@ -0,0 +1,513 @@ +# Natural Language Toolkit: NKJP Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Gabriela Kaczka +# URL: +# For license information, see LICENSE.TXT + +import functools +import os +import re +import tempfile + +from nltk.corpus.reader.util import concat +from nltk.corpus.reader.xmldocs import XMLCorpusReader, XMLCorpusView + + +def _parse_args(fun): + """ + Wraps function arguments: + if fileids not specified then function set NKJPCorpusReader paths. + """ + + @functools.wraps(fun) + def decorator(self, fileids=None, **kwargs): + if not fileids: + fileids = self._paths + return fun(self, fileids, **kwargs) + + return decorator + + +class NKJPCorpusReader(XMLCorpusReader): + WORDS_MODE = 0 + SENTS_MODE = 1 + HEADER_MODE = 2 + RAW_MODE = 3 + + def __init__(self, root, fileids=".*"): + """ + Corpus reader designed to work with National Corpus of Polish. + See http://nkjp.pl/ for more details about NKJP. + use example: + import nltk + import nkjp + from nkjp import NKJPCorpusReader + x = NKJPCorpusReader(root='/home/USER/nltk_data/corpora/nkjp/', fileids='') # obtain the whole corpus + x.header() + x.raw() + x.words() + x.tagged_words(tags=['subst', 'comp']) #Link to find more tags: nkjp.pl/poliqarp/help/ense2.html + x.sents() + x = NKJPCorpusReader(root='/home/USER/nltk_data/corpora/nkjp/', fileids='Wilk*') # obtain particular file(s) + x.header(fileids=['WilkDom', '/home/USER/nltk_data/corpora/nkjp/WilkWilczy']) + x.tagged_words(fileids=['WilkDom', '/home/USER/nltk_data/corpora/nkjp/WilkWilczy'], tags=['subst', 'comp']) + """ + if isinstance(fileids, str): + XMLCorpusReader.__init__(self, root, fileids + ".*/header.xml") + else: + XMLCorpusReader.__init__( + self, root, [fileid + "/header.xml" for fileid in fileids] + ) + self._paths = self.get_paths() + + def get_paths(self): + return [ + os.path.join(str(self._root), f.split("header.xml")[0]) + for f in self._fileids + ] + + def fileids(self): + """ + Returns a list of file identifiers for the fileids that make up + this corpus. + """ + return [f.split("header.xml")[0] for f in self._fileids] + + def _view(self, filename, tags=None, **kwargs): + """ + Returns a view specialised for use with particular corpus file. + """ + mode = kwargs.pop("mode", NKJPCorpusReader.WORDS_MODE) + if mode is NKJPCorpusReader.WORDS_MODE: + return NKJPCorpus_Morph_View(filename, tags=tags) + elif mode is NKJPCorpusReader.SENTS_MODE: + return NKJPCorpus_Segmentation_View(filename, tags=tags) + elif mode is NKJPCorpusReader.HEADER_MODE: + return NKJPCorpus_Header_View(filename, tags=tags) + elif mode is NKJPCorpusReader.RAW_MODE: + return NKJPCorpus_Text_View( + filename, tags=tags, mode=NKJPCorpus_Text_View.RAW_MODE + ) + + else: + raise NameError("No such mode!") + + def add_root(self, fileid): + """ + Add root if necessary to specified fileid, and verify the resulting + path stays inside the corpus root. + + Security (CWE-22): the NKJP views build file paths from the + caller-supplied ``fileids`` and read them with the builtin + ``open()``, bypassing the ``CorpusReader.open()`` / ``nltk.pathsec`` + sandbox. Route the resulting path through + ``nltk.pathsec.validate_path()`` with the corpus root as + ``required_root`` -- the same symlink-resolving containment guard used + by ``CorpusReader.open()`` (PR #3528) -- so that a ``..`` sequence, an + absolute path, or a symlink in ``fileids`` cannot escape the corpus + root. + """ + from nltk.pathsec import validate_path + + # ``str(self.root)`` is the original (un-normalised) constructor + # argument; abspath() gives the platform-native absolute root that + # ``os.path.join`` expects (the old substring/concatenation logic + # duplicated the root on Windows, where the separators differ). + root = os.path.abspath(str(self.root)) + fileid = str(fileid) + if os.path.isabs(fileid): + result = fileid + else: + result = os.path.join(root, fileid) + # Symlink-aware containment: validate_path() resolves both the + # candidate path and the root (``Path(...).resolve()``) before + # comparing, and raises ValueError if the resolved path leaves the + # corpus root -- unlike os.path.abspath(), which does not follow + # symlinks, so an in-root symlink could otherwise point outside. + validate_path(result, context="NKJPCorpusReader", required_root=self.root) + return result + + @_parse_args + def header(self, fileids=None, **kwargs): + """ + Returns header(s) of specified fileids. + """ + return concat( + [ + self._view( + self.add_root(fileid), mode=NKJPCorpusReader.HEADER_MODE, **kwargs + ).handle_query() + for fileid in fileids + ] + ) + + @_parse_args + def sents(self, fileids=None, **kwargs): + """ + Returns sentences in specified fileids. + """ + return concat( + [ + self._view( + self.add_root(fileid), mode=NKJPCorpusReader.SENTS_MODE, **kwargs + ).handle_query() + for fileid in fileids + ] + ) + + @_parse_args + def words(self, fileids=None, **kwargs): + """ + Returns words in specified fileids. + """ + + return concat( + [ + self._view( + self.add_root(fileid), mode=NKJPCorpusReader.WORDS_MODE, **kwargs + ).handle_query() + for fileid in fileids + ] + ) + + @_parse_args + def tagged_words(self, fileids=None, **kwargs): + """ + Call with specified tags as a list, e.g. tags=['subst', 'comp']. + Returns tagged words in specified fileids. + """ + tags = kwargs.pop("tags", []) + return concat( + [ + self._view( + self.add_root(fileid), + mode=NKJPCorpusReader.WORDS_MODE, + tags=tags, + **kwargs, + ).handle_query() + for fileid in fileids + ] + ) + + @_parse_args + def raw(self, fileids=None, **kwargs): + """ + Returns words in specified fileids. + """ + return concat( + [ + self._view( + self.add_root(fileid), mode=NKJPCorpusReader.RAW_MODE, **kwargs + ).handle_query() + for fileid in fileids + ] + ) + + +class NKJPCorpus_Header_View(XMLCorpusView): + def __init__(self, filename, **kwargs): + """ + HEADER_MODE + A stream backed corpus view specialized for use with + header.xml files in NKJP corpus. + """ + self.tagspec = ".*/sourceDesc$" + XMLCorpusView.__init__(self, filename + "header.xml", self.tagspec) + + def handle_query(self): + self._open() + header = [] + while True: + segm = XMLCorpusView.read_block(self, self._stream) + if len(segm) == 0: + break + header.extend(segm) + self.close() + return header + + def handle_elt(self, elt, context): + titles = elt.findall("bibl/title") + title = [] + if titles: + title = "\n".join(title.text.strip() for title in titles) + + authors = elt.findall("bibl/author") + author = [] + if authors: + author = "\n".join(author.text.strip() for author in authors) + + dates = elt.findall("bibl/date") + date = [] + if dates: + date = "\n".join(date.text.strip() for date in dates) + + publishers = elt.findall("bibl/publisher") + publisher = [] + if publishers: + publisher = "\n".join(publisher.text.strip() for publisher in publishers) + + idnos = elt.findall("bibl/idno") + idno = [] + if idnos: + idno = "\n".join(idno.text.strip() for idno in idnos) + + notes = elt.findall("bibl/note") + note = [] + if notes: + note = "\n".join(note.text.strip() for note in notes) + + return { + "title": title, + "author": author, + "date": date, + "publisher": publisher, + "idno": idno, + "note": note, + } + + +class XML_Tool: + """ + Helper class creating xml file to one without references to nkjp: namespace. + That's needed because the XMLCorpusView assumes that one can find short substrings + of XML that are valid XML, which is not true if a namespace is declared at top level + """ + + def __init__(self, root, filename): + self.read_file = os.path.join(root, filename) + self.write_file = tempfile.NamedTemporaryFile(delete=False) + + def build_preprocessed_file(self): + try: + fr = open(self.read_file) + fw = self.write_file + line = " " + while len(line): + line = fr.readline() + x = re.split(r"nkjp:[^ ]* ", line) # in all files + ret = " ".join(x) + x = re.split("", ret) # in ann_segmentation.xml + ret = " ".join(x) + x = re.split("", ret) # in ann_segmentation.xml + ret = " ".join(x) + x = re.split("", ret) # in ann_segmentation.xml + ret = " ".join(x) + x = re.split("", ret) # in ann_segmentation.xml + ret = " ".join(x) + fw.write(ret) + fr.close() + fw.close() + return self.write_file.name + except Exception as e: + self.remove_preprocessed_file() + raise Exception from e + + def remove_preprocessed_file(self): + os.remove(self.write_file.name) + + +class NKJPCorpus_Segmentation_View(XMLCorpusView): + """ + A stream backed corpus view specialized for use with + ann_segmentation.xml files in NKJP corpus. + """ + + def __init__(self, filename, **kwargs): + self.tagspec = ".*p/.*s" + # intersperse NKJPCorpus_Text_View + self.text_view = NKJPCorpus_Text_View( + filename, mode=NKJPCorpus_Text_View.SENTS_MODE + ) + self.text_view.handle_query() + # xml preprocessing + self.xml_tool = XML_Tool(filename, "ann_segmentation.xml") + # base class init + XMLCorpusView.__init__( + self, self.xml_tool.build_preprocessed_file(), self.tagspec + ) + + def get_segm_id(self, example_word): + return example_word.split("(")[1].split(",")[0] + + def get_sent_beg(self, beg_word): + # returns index of beginning letter in sentence + return int(beg_word.split(",")[1]) + + def get_sent_end(self, end_word): + # returns index of end letter in sentence + splitted = end_word.split(")")[0].split(",") + return int(splitted[1]) + int(splitted[2]) + + def get_sentences(self, sent_segm): + # returns one sentence + id = self.get_segm_id(sent_segm[0]) + segm = self.text_view.segm_dict[id] # text segment + beg = self.get_sent_beg(sent_segm[0]) + end = self.get_sent_end(sent_segm[len(sent_segm) - 1]) + return segm[beg:end] + + def remove_choice(self, segm): + ret = [] + prev_txt_end = -1 + prev_txt_nr = -1 + for word in segm: + txt_nr = self.get_segm_id(word) + # get increasing sequence of ids: in case of choice get first possibility + if self.get_sent_beg(word) > prev_txt_end - 1 or prev_txt_nr != txt_nr: + ret.append(word) + prev_txt_end = self.get_sent_end(word) + prev_txt_nr = txt_nr + + return ret + + def handle_query(self): + try: + self._open() + sentences = [] + while True: + sent_segm = XMLCorpusView.read_block(self, self._stream) + if len(sent_segm) == 0: + break + for segm in sent_segm: + segm = self.remove_choice(segm) + sentences.append(self.get_sentences(segm)) + self.close() + self.xml_tool.remove_preprocessed_file() + return sentences + except Exception as e: + self.xml_tool.remove_preprocessed_file() + raise Exception from e + + def handle_elt(self, elt, context): + ret = [] + for seg in elt: + ret.append(seg.get("corresp")) + return ret + + +class NKJPCorpus_Text_View(XMLCorpusView): + """ + A stream backed corpus view specialized for use with + text.xml files in NKJP corpus. + """ + + SENTS_MODE = 0 + RAW_MODE = 1 + + def __init__(self, filename, **kwargs): + self.mode = kwargs.pop("mode", 0) + self.tagspec = ".*/div/ab" + self.segm_dict = dict() + # xml preprocessing + self.xml_tool = XML_Tool(filename, "text.xml") + # base class init + XMLCorpusView.__init__( + self, self.xml_tool.build_preprocessed_file(), self.tagspec + ) + + def handle_query(self): + try: + self._open() + x = self.read_block(self._stream) + self.close() + self.xml_tool.remove_preprocessed_file() + return x + except Exception as e: + self.xml_tool.remove_preprocessed_file() + raise Exception from e + + def read_block(self, stream, tagspec=None, elt_handler=None): + """ + Returns text as a list of sentences. + """ + txt = [] + while True: + segm = XMLCorpusView.read_block(self, stream) + if len(segm) == 0: + break + for part in segm: + txt.append(part) + + return [" ".join([segm for segm in txt])] + + def get_segm_id(self, elt): + for attr in elt.attrib: + if attr.endswith("id"): + return elt.get(attr) + + def handle_elt(self, elt, context): + # fill dictionary to use later in sents mode + if self.mode is NKJPCorpus_Text_View.SENTS_MODE: + self.segm_dict[self.get_segm_id(elt)] = elt.text + return elt.text + + +class NKJPCorpus_Morph_View(XMLCorpusView): + """ + A stream backed corpus view specialized for use with + ann_morphosyntax.xml files in NKJP corpus. + """ + + def __init__(self, filename, **kwargs): + self.tags = kwargs.pop("tags", None) + self.tagspec = ".*/seg/fs" + self.xml_tool = XML_Tool(filename, "ann_morphosyntax.xml") + XMLCorpusView.__init__( + self, self.xml_tool.build_preprocessed_file(), self.tagspec + ) + + def handle_query(self): + try: + self._open() + words = [] + while True: + segm = XMLCorpusView.read_block(self, self._stream) + if len(segm) == 0: + break + for part in segm: + if part is not None: + words.append(part) + self.close() + self.xml_tool.remove_preprocessed_file() + return words + except Exception as e: + self.xml_tool.remove_preprocessed_file() + raise Exception from e + + def handle_elt(self, elt, context): + word = "" + flag = False + is_not_interp = True + # if tags not specified, then always return word + if self.tags is None: + flag = True + + for child in elt: + # get word + if "name" in child.keys() and child.attrib["name"] == "orth": + for symbol in child: + if symbol.tag == "string": + word = symbol.text + elif "name" in child.keys() and child.attrib["name"] == "interps": + for symbol in child: + if "type" in symbol.keys() and symbol.attrib["type"] == "lex": + for symbol2 in symbol: + if ( + "name" in symbol2.keys() + and symbol2.attrib["name"] == "ctag" + ): + for symbol3 in symbol2: + if ( + "value" in symbol3.keys() + and self.tags is not None + and symbol3.attrib["value"] in self.tags + ): + flag = True + elif ( + "value" in symbol3.keys() + and symbol3.attrib["value"] == "interp" + ): + is_not_interp = False + if flag and is_not_interp: + return word diff --git a/nltk/corpus/reader/nombank.py b/nltk/corpus/reader/nombank.py new file mode 100644 index 0000000..788a0a1 --- /dev/null +++ b/nltk/corpus/reader/nombank.py @@ -0,0 +1,480 @@ +# Natural Language Toolkit: NomBank Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Paul Bedaride +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +from functools import total_ordering + +from defusedxml.ElementTree import parse as safe_parse + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.internals import raise_unorderable_types +from nltk.tree import Tree + + +class NombankCorpusReader(CorpusReader): + """ + Corpus reader for the nombank corpus, which augments the Penn + Treebank with information about the predicate argument structure + of every noun instance. The corpus consists of two parts: the + predicate-argument annotations themselves, and a set of "frameset + files" which define the argument labels used by the annotations, + on a per-noun basis. Each "frameset file" contains one or more + predicates, such as ``'turn'`` or ``'turn_on'``, each of which is + divided into coarse-grained word senses called "rolesets". For + each "roleset", the frameset file provides descriptions of the + argument roles, along with examples. + """ + + def __init__( + self, + root, + nomfile, + framefiles="", + nounsfile=None, + parse_fileid_xform=None, + parse_corpus=None, + encoding="utf8", + ): + """ + :param root: The root directory for this corpus. + :param nomfile: The name of the file containing the predicate- + argument annotations (relative to ``root``). + :param framefiles: A list or regexp specifying the frameset + fileids for this corpus. + :param parse_fileid_xform: A transform that should be applied + to the fileids in this corpus. This should be a function + of one argument (a fileid) that returns a string (the new + fileid). + :param parse_corpus: The corpus containing the parse trees + corresponding to this corpus. These parse trees are + necessary to resolve the tree pointers used by nombank. + """ + + # If framefiles is specified as a regexp, expand it. + if isinstance(framefiles, str): + self._fileids = find_corpus_fileids(root, framefiles) + self._fileids = list(framefiles) + # Initialize the corpus reader. + CorpusReader.__init__(self, root, framefiles, encoding) + + # Record our nom file & nouns file. + self._nomfile = nomfile + self._nounsfile = nounsfile + self._parse_fileid_xform = parse_fileid_xform + self._parse_corpus = parse_corpus + + def instances(self, baseform=None): + """ + :return: a corpus view that acts as a list of + ``NombankInstance`` objects, one for each noun in the corpus. + """ + kwargs = {} + if baseform is not None: + kwargs["instance_filter"] = lambda inst: inst.baseform == baseform + return StreamBackedCorpusView( + self.abspath(self._nomfile), + lambda stream: self._read_instance_block(stream, **kwargs), + encoding=self.encoding(self._nomfile), + ) + + def lines(self): + """ + :return: a corpus view that acts as a list of strings, one for + each line in the predicate-argument annotation file. + """ + return StreamBackedCorpusView( + self.abspath(self._nomfile), + read_line_block, + encoding=self.encoding(self._nomfile), + ) + + def roleset(self, roleset_id): + """ + :return: the xml description for the given roleset. + """ + baseform = roleset_id.split(".")[0] + baseform = baseform.replace("perc-sign", "%") + baseform = baseform.replace("oneslashonezero", "1/10").replace( + "1/10", "1-slash-10" + ) + framefile = "frames/%s.xml" % baseform + if framefile not in self.fileids(): + raise ValueError("Frameset file for %s not found" % roleset_id) + + # n.b.: The encoding for XML fileids is specified by the file + # itself; so we ignore self._encoding here. + with self.abspath(framefile).open() as fp: + etree = safe_parse(fp).getroot() + for roleset in etree.findall("predicate/roleset"): + if roleset.attrib["id"] == roleset_id: + return roleset + raise ValueError(f"Roleset {roleset_id} not found in {framefile}") + + def rolesets(self, baseform=None): + """ + :return: list of xml descriptions for rolesets. + """ + if baseform is not None: + framefile = "frames/%s.xml" % baseform + if framefile not in self.fileids(): + raise ValueError("Frameset file for %s not found" % baseform) + framefiles = [framefile] + else: + framefiles = self.fileids() + + rsets = [] + for framefile in framefiles: + # n.b.: The encoding for XML fileids is specified by the file + # itself; so we ignore self._encoding here. + with self.abspath(framefile).open() as fp: + etree = safe_parse(fp).getroot() + rsets.append(etree.findall("predicate/roleset")) + return LazyConcatenation(rsets) + + def nouns(self): + """ + :return: a corpus view that acts as a list of all noun lemmas + in this corpus (from the nombank.1.0.words file). + """ + return StreamBackedCorpusView( + self.abspath(self._nounsfile), + read_line_block, + encoding=self.encoding(self._nounsfile), + ) + + def _read_instance_block(self, stream, instance_filter=lambda inst: True): + block = [] + + # Read 100 at a time. + for i in range(100): + line = stream.readline().strip() + if line: + inst = NombankInstance.parse( + line, self._parse_fileid_xform, self._parse_corpus + ) + if instance_filter(inst): + block.append(inst) + + return block + + +###################################################################### +# { Nombank Instance & related datatypes +###################################################################### + + +class NombankInstance: + def __init__( + self, + fileid, + sentnum, + wordnum, + baseform, + sensenumber, + predicate, + predid, + arguments, + parse_corpus=None, + ): + self.fileid = fileid + """The name of the file containing the parse tree for this + instance's sentence.""" + + self.sentnum = sentnum + """The sentence number of this sentence within ``fileid``. + Indexing starts from zero.""" + + self.wordnum = wordnum + """The word number of this instance's predicate within its + containing sentence. Word numbers are indexed starting from + zero, and include traces and other empty parse elements.""" + + self.baseform = baseform + """The baseform of the predicate.""" + + self.sensenumber = sensenumber + """The sense number of the predicate.""" + + self.predicate = predicate + """A ``NombankTreePointer`` indicating the position of this + instance's predicate within its containing sentence.""" + + self.predid = predid + """Identifier of the predicate.""" + + self.arguments = tuple(arguments) + """A list of tuples (argloc, argid), specifying the location + and identifier for each of the predicate's argument in the + containing sentence. Argument identifiers are strings such as + ``'ARG0'`` or ``'ARGM-TMP'``. This list does *not* contain + the predicate.""" + + self.parse_corpus = parse_corpus + """A corpus reader for the parse trees corresponding to the + instances in this nombank corpus.""" + + @property + def roleset(self): + """The name of the roleset used by this instance's predicate. + Use ``nombank.roleset() `` to + look up information about the roleset.""" + r = self.baseform.replace("%", "perc-sign") + r = r.replace("1/10", "1-slash-10").replace("1-slash-10", "oneslashonezero") + return f"{r}.{self.sensenumber}" + + def __repr__(self): + return "".format( + self.fileid, + self.sentnum, + self.wordnum, + ) + + def __str__(self): + s = "{} {} {} {} {}".format( + self.fileid, + self.sentnum, + self.wordnum, + self.baseform, + self.sensenumber, + ) + items = self.arguments + ((self.predicate, "rel"),) + for argloc, argid in sorted(items): + s += f" {argloc}-{argid}" + return s + + def _get_tree(self): + if self.parse_corpus is None: + return None + if self.fileid not in self.parse_corpus.fileids(): + return None + return self.parse_corpus.parsed_sents(self.fileid)[self.sentnum] + + tree = property( + _get_tree, + doc=""" + The parse tree corresponding to this instance, or None if + the corresponding tree is not available.""", + ) + + @staticmethod + def parse(s, parse_fileid_xform=None, parse_corpus=None): + pieces = s.split() + if len(pieces) < 6: + raise ValueError("Badly formatted nombank line: %r" % s) + + # Divide the line into its basic pieces. + (fileid, sentnum, wordnum, baseform, sensenumber) = pieces[:5] + + args = pieces[5:] + rel = [args.pop(i) for i, p in enumerate(args) if "-rel" in p] + if len(rel) != 1: + raise ValueError("Badly formatted nombank line: %r" % s) + + # Apply the fileid selector, if any. + if parse_fileid_xform is not None: + fileid = parse_fileid_xform(fileid) + + # Convert sentence & word numbers to ints. A non-numeric field would + # otherwise raise a cryptic ValueError that aborts iteration over the + # whole corpus; fail with the same clear message as the checks above. + try: + sentnum = int(sentnum) + wordnum = int(wordnum) + except ValueError: + raise ValueError("Badly formatted nombank line: %r" % s) from None + + # Parse the predicate location. (The rel field always contains "-rel", + # so the split is defensive, but check it so a future change to the + # rel selection can't crash with a cryptic unpacking error.) + pred_pieces = rel[0].split("-", 1) + if len(pred_pieces) != 2: + raise ValueError("Badly formatted nombank line: %r" % s) + predloc, predid = pred_pieces + predicate = NombankTreePointer.parse(predloc) + + # Parse the arguments. + arguments = [] + for arg in args: + arg_pieces = arg.split("-", 1) + # An argument missing its "-" separator would otherwise raise a + # cryptic unpacking ValueError that aborts the whole-corpus iteration. + if len(arg_pieces) != 2: + raise ValueError("Badly formatted nombank line: %r" % s) + argloc, argid = arg_pieces + arguments.append((NombankTreePointer.parse(argloc), argid)) + + # Put it all together. + return NombankInstance( + fileid, + sentnum, + wordnum, + baseform, + sensenumber, + predicate, + predid, + arguments, + parse_corpus, + ) + + +class NombankPointer: + """ + A pointer used by nombank to identify one or more constituents in + a parse tree. ``NombankPointer`` is an abstract base class with + three concrete subclasses: + + - ``NombankTreePointer`` is used to point to single constituents. + - ``NombankSplitTreePointer`` is used to point to 'split' + constituents, which consist of a sequence of two or more + ``NombankTreePointer`` pointers. + - ``NombankChainTreePointer`` is used to point to entire trace + chains in a tree. It consists of a sequence of pieces, which + can be ``NombankTreePointer`` or ``NombankSplitTreePointer`` pointers. + """ + + def __init__(self): + if self.__class__ == NombankPointer: + raise NotImplementedError() + + +class NombankChainTreePointer(NombankPointer): + def __init__(self, pieces): + self.pieces = pieces + """A list of the pieces that make up this chain. Elements may + be either ``NombankSplitTreePointer`` or + ``NombankTreePointer`` pointers.""" + + def __str__(self): + return "*".join("%s" % p for p in self.pieces) + + def __repr__(self): + return "" % self + + def select(self, tree): + if tree is None: + raise ValueError("Parse tree not available") + return Tree("*CHAIN*", [p.select(tree) for p in self.pieces]) + + +class NombankSplitTreePointer(NombankPointer): + def __init__(self, pieces): + self.pieces = pieces + """A list of the pieces that make up this chain. Elements are + all ``NombankTreePointer`` pointers.""" + + def __str__(self): + return ",".join("%s" % p for p in self.pieces) + + def __repr__(self): + return "" % self + + def select(self, tree): + if tree is None: + raise ValueError("Parse tree not available") + return Tree("*SPLIT*", [p.select(tree) for p in self.pieces]) + + +@total_ordering +class NombankTreePointer(NombankPointer): + """ + wordnum:height*wordnum:height*... + wordnum:height, + + """ + + def __init__(self, wordnum, height): + self.wordnum = wordnum + self.height = height + + @staticmethod + def parse(s): + # Deal with chains (xx*yy*zz) + pieces = s.split("*") + if len(pieces) > 1: + return NombankChainTreePointer( + [NombankTreePointer.parse(elt) for elt in pieces] + ) + + # Deal with split args (xx,yy,zz) + pieces = s.split(",") + if len(pieces) > 1: + return NombankSplitTreePointer( + [NombankTreePointer.parse(elt) for elt in pieces] + ) + + # Deal with normal pointers. + pieces = s.split(":") + if len(pieces) != 2: + raise ValueError("bad nombank pointer %r" % s) + return NombankTreePointer(int(pieces[0]), int(pieces[1])) + + def __str__(self): + return f"{self.wordnum}:{self.height}" + + def __repr__(self): + return "NombankTreePointer(%d, %d)" % (self.wordnum, self.height) + + def __eq__(self, other): + while isinstance(other, (NombankChainTreePointer, NombankSplitTreePointer)): + other = other.pieces[0] + + if not isinstance(other, NombankTreePointer): + return self is other + + return self.wordnum == other.wordnum and self.height == other.height + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + while isinstance(other, (NombankChainTreePointer, NombankSplitTreePointer)): + other = other.pieces[0] + + if not isinstance(other, NombankTreePointer): + return id(self) < id(other) + + return (self.wordnum, -self.height) < (other.wordnum, -other.height) + + def select(self, tree): + if tree is None: + raise ValueError("Parse tree not available") + return tree[self.treepos(tree)] + + def treepos(self, tree): + """ + Convert this pointer to a standard 'tree position' pointer, + given that it points to the given tree. + """ + if tree is None: + raise ValueError("Parse tree not available") + stack = [tree] + treepos = [] + + wordnum = 0 + while True: + # tree node: + if isinstance(stack[-1], Tree): + # Select the next child. + if len(treepos) < len(stack): + treepos.append(0) + else: + treepos[-1] += 1 + # Update the stack. + if treepos[-1] < len(stack[-1]): + stack.append(stack[-1][treepos[-1]]) + else: + # End of node's child list: pop up a level. + stack.pop() + treepos.pop() + # word node: + else: + if wordnum == self.wordnum: + return tuple(treepos[: len(treepos) - self.height - 1]) + else: + wordnum += 1 + stack.pop() diff --git a/nltk/corpus/reader/nps_chat.py b/nltk/corpus/reader/nps_chat.py new file mode 100644 index 0000000..d85f36e --- /dev/null +++ b/nltk/corpus/reader/nps_chat.py @@ -0,0 +1,90 @@ +# Natural Language Toolkit: NPS Chat Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +import re +import textwrap + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.corpus.reader.xmldocs import * +from nltk.internals import ElementWrapper +from nltk.tag import map_tag +from nltk.util import LazyConcatenation + + +class NPSChatCorpusReader(XMLCorpusReader): + def __init__(self, root, fileids, wrap_etree=False, tagset=None): + XMLCorpusReader.__init__(self, root, fileids, wrap_etree) + self._tagset = tagset + + def xml_posts(self, fileids=None): + if self._wrap_etree: + return concat( + [ + XMLCorpusView(fileid, "Session/Posts/Post", self._wrap_elt) + for fileid in self.abspaths(fileids) + ] + ) + else: + return concat( + [ + XMLCorpusView(fileid, "Session/Posts/Post") + for fileid in self.abspaths(fileids) + ] + ) + + def posts(self, fileids=None): + return concat( + [ + XMLCorpusView( + fileid, "Session/Posts/Post/terminals", self._elt_to_words + ) + for fileid in self.abspaths(fileids) + ] + ) + + def tagged_posts(self, fileids=None, tagset=None): + def reader(elt, handler): + return self._elt_to_tagged_words(elt, handler, tagset) + + return concat( + [ + XMLCorpusView(fileid, "Session/Posts/Post/terminals", reader) + for fileid in self.abspaths(fileids) + ] + ) + + def words(self, fileids=None): + return LazyConcatenation(self.posts(fileids)) + + def tagged_words(self, fileids=None, tagset=None): + return LazyConcatenation(self.tagged_posts(fileids, tagset)) + + def _wrap_elt(self, elt, handler): + return ElementWrapper(elt) + + def _elt_to_words(self, elt, handler): + return [self._simplify_username(t.attrib["word"]) for t in elt.findall("t")] + + def _elt_to_tagged_words(self, elt, handler, tagset=None): + tagged_post = [ + (self._simplify_username(t.attrib["word"]), t.attrib["pos"]) + for t in elt.findall("t") + ] + if tagset and tagset != self._tagset: + tagged_post = [ + (w, map_tag(self._tagset, tagset, t)) for (w, t) in tagged_post + ] + return tagged_post + + @staticmethod + def _simplify_username(word): + if "User" in word: + word = "U" + word.split("User", 1)[1] + elif isinstance(word, bytes): + word = word.decode("ascii") + return word diff --git a/nltk/corpus/reader/opinion_lexicon.py b/nltk/corpus/reader/opinion_lexicon.py new file mode 100644 index 0000000..5beb781 --- /dev/null +++ b/nltk/corpus/reader/opinion_lexicon.py @@ -0,0 +1,125 @@ +# Natural Language Toolkit: Opinion Lexicon Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +CorpusReader for the Opinion Lexicon. + +Opinion Lexicon information +=========================== + +Authors: Minqing Hu and Bing Liu, 2004. + Department of Computer Science + University of Illinois at Chicago + +Contact: Bing Liu, liub@cs.uic.edu + https://www.cs.uic.edu/~liub + +Distributed with permission. + +Related papers: + +- Minqing Hu and Bing Liu. "Mining and summarizing customer reviews". + Proceedings of the ACM SIGKDD International Conference on Knowledge Discovery + & Data Mining (KDD-04), Aug 22-25, 2004, Seattle, Washington, USA. + +- Bing Liu, Minqing Hu and Junsheng Cheng. "Opinion Observer: Analyzing and + Comparing Opinions on the Web". Proceedings of the 14th International World + Wide Web conference (WWW-2005), May 10-14, 2005, Chiba, Japan. +""" + +from nltk.corpus.reader import WordListCorpusReader +from nltk.corpus.reader.api import * + + +class IgnoreReadmeCorpusView(StreamBackedCorpusView): + """ + This CorpusView is used to skip the initial readme block of the corpus. + """ + + def __init__(self, *args, **kwargs): + StreamBackedCorpusView.__init__(self, *args, **kwargs) + # open self._stream + self._open() + # skip the readme block + read_blankline_block(self._stream) + # Set the initial position to the current stream position + self._filepos = [self._stream.tell()] + + +class OpinionLexiconCorpusReader(WordListCorpusReader): + """ + Reader for Liu and Hu opinion lexicon. Blank lines and readme are ignored. + + >>> from nltk.corpus import opinion_lexicon + >>> opinion_lexicon.words() + ['2-faced', '2-faces', 'abnormal', 'abolish', ...] + + The OpinionLexiconCorpusReader provides shortcuts to retrieve positive/negative + words: + + >>> opinion_lexicon.negative() + ['2-faced', '2-faces', 'abnormal', 'abolish', ...] + + Note that words from `words()` method are sorted by file id, not alphabetically: + + >>> opinion_lexicon.words()[0:10] # doctest: +NORMALIZE_WHITESPACE + ['2-faced', '2-faces', 'abnormal', 'abolish', 'abominable', 'abominably', + 'abominate', 'abomination', 'abort', 'aborted'] + >>> sorted(opinion_lexicon.words())[0:10] # doctest: +NORMALIZE_WHITESPACE + ['2-faced', '2-faces', 'a+', 'abnormal', 'abolish', 'abominable', 'abominably', + 'abominate', 'abomination', 'abort'] + """ + + CorpusView = IgnoreReadmeCorpusView + + def words(self, fileids=None): + """ + Return all words in the opinion lexicon. Note that these words are not + sorted in alphabetical order. + + :param fileids: a list or regexp specifying the ids of the files whose + words have to be returned. + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(path, self._read_word_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def positive(self): + """ + Return all positive words in alphabetical order. + + :return: a list of positive words. + :rtype: list(str) + """ + return self.words("positive-words.txt") + + def negative(self): + """ + Return all negative words in alphabetical order. + + :return: a list of negative words. + :rtype: list(str) + """ + return self.words("negative-words.txt") + + def _read_word_block(self, stream): + words = [] + for i in range(20): # Read 20 lines at a time. + line = stream.readline() + if not line: + continue + words.append(line.strip()) + return words diff --git a/nltk/corpus/reader/panlex_lite.py b/nltk/corpus/reader/panlex_lite.py new file mode 100644 index 0000000..fb30dfc --- /dev/null +++ b/nltk/corpus/reader/panlex_lite.py @@ -0,0 +1,174 @@ +# Natural Language Toolkit: PanLex Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: David Kamholz +# URL: +# For license information, see LICENSE.TXT + +""" +CorpusReader for PanLex Lite, a stripped down version of PanLex distributed +as an SQLite database. See the README.txt in the panlex_lite corpus directory +for more information on PanLex Lite. +""" + +import os +import sqlite3 + +from nltk.corpus.reader.api import CorpusReader + + +class PanLexLiteCorpusReader(CorpusReader): + MEANING_Q = """ + SELECT dnx2.mn, dnx2.uq, dnx2.ap, dnx2.ui, ex2.tt, ex2.lv + FROM dnx + JOIN ex ON (ex.ex = dnx.ex) + JOIN dnx dnx2 ON (dnx2.mn = dnx.mn) + JOIN ex ex2 ON (ex2.ex = dnx2.ex) + WHERE dnx.ex != dnx2.ex AND ex.tt = ? AND ex.lv = ? + ORDER BY dnx2.uq DESC + """ + + TRANSLATION_Q = """ + SELECT s.tt, sum(s.uq) AS trq FROM ( + SELECT ex2.tt, max(dnx.uq) AS uq + FROM dnx + JOIN ex ON (ex.ex = dnx.ex) + JOIN dnx dnx2 ON (dnx2.mn = dnx.mn) + JOIN ex ex2 ON (ex2.ex = dnx2.ex) + WHERE dnx.ex != dnx2.ex AND ex.lv = ? AND ex.tt = ? AND ex2.lv = ? + GROUP BY ex2.tt, dnx.ui + ) s + GROUP BY s.tt + ORDER BY trq DESC, s.tt + """ + + def __init__(self, root): + self._c = sqlite3.connect(os.path.join(root, "db.sqlite")).cursor() + + self._uid_lv = {} + self._lv_uid = {} + + for row in self._c.execute("SELECT uid, lv FROM lv"): + self._uid_lv[row[0]] = row[1] + self._lv_uid[row[1]] = row[0] + + def language_varieties(self, lc=None): + """ + Return a list of PanLex language varieties. + + :param lc: ISO 639 alpha-3 code. If specified, filters returned varieties + by this code. If unspecified, all varieties are returned. + :return: the specified language varieties as a list of tuples. The first + element is the language variety's seven-character uniform identifier, + and the second element is its default name. + :rtype: list(tuple) + """ + + if lc is None: + return self._c.execute("SELECT uid, tt FROM lv ORDER BY uid").fetchall() + else: + return self._c.execute( + "SELECT uid, tt FROM lv WHERE lc = ? ORDER BY uid", (lc,) + ).fetchall() + + def meanings(self, expr_uid, expr_tt): + """ + Return a list of meanings for an expression. + + :param expr_uid: the expression's language variety, as a seven-character + uniform identifier. + :param expr_tt: the expression's text. + :return: a list of Meaning objects. + :rtype: list(Meaning) + """ + + expr_lv = self._uid_lv[expr_uid] + + mn_info = {} + + for i in self._c.execute(self.MEANING_Q, (expr_tt, expr_lv)): + mn = i[0] + uid = self._lv_uid[i[5]] + + if mn not in mn_info: + mn_info[mn] = { + "uq": i[1], + "ap": i[2], + "ui": i[3], + "ex": {expr_uid: [expr_tt]}, + } + + if uid not in mn_info[mn]["ex"]: + mn_info[mn]["ex"][uid] = [] + + mn_info[mn]["ex"][uid].append(i[4]) + + return [Meaning(mn, mn_info[mn]) for mn in mn_info] + + def translations(self, from_uid, from_tt, to_uid): + """ + Return a list of translations for an expression into a single language + variety. + + :param from_uid: the source expression's language variety, as a + seven-character uniform identifier. + :param from_tt: the source expression's text. + :param to_uid: the target language variety, as a seven-character + uniform identifier. + :return: a list of translation tuples. The first element is the expression + text and the second element is the translation quality. + :rtype: list(tuple) + """ + + from_lv = self._uid_lv[from_uid] + to_lv = self._uid_lv[to_uid] + + return self._c.execute(self.TRANSLATION_Q, (from_lv, from_tt, to_lv)).fetchall() + + +class Meaning(dict): + """ + Represents a single PanLex meaning. A meaning is a translation set derived + from a single source. + """ + + def __init__(self, mn, attr): + super().__init__(**attr) + self["mn"] = mn + + def id(self): + """ + :return: the meaning's id. + :rtype: int + """ + return self["mn"] + + def quality(self): + """ + :return: the meaning's source's quality (0=worst, 9=best). + :rtype: int + """ + return self["uq"] + + def source(self): + """ + :return: the meaning's source id. + :rtype: int + """ + return self["ap"] + + def source_group(self): + """ + :return: the meaning's source group id. + :rtype: int + """ + return self["ui"] + + def expressions(self): + """ + :return: the meaning's expressions as a dictionary whose keys are language + variety uniform identifiers and whose values are lists of expression + texts. + :rtype: dict + """ + return self["ex"] diff --git a/nltk/corpus/reader/panlex_swadesh.py b/nltk/corpus/reader/panlex_swadesh.py new file mode 100644 index 0000000..5f21b94 --- /dev/null +++ b/nltk/corpus/reader/panlex_swadesh.py @@ -0,0 +1,95 @@ +# Natural Language Toolkit: Word List Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + + +import re +from collections import defaultdict, namedtuple + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.corpus.reader.wordlist import WordListCorpusReader +from nltk.tokenize import line_tokenize + +PanlexLanguage = namedtuple( + "PanlexLanguage", + [ + "panlex_uid", # (1) PanLex UID + "iso639", # (2) ISO 639 language code + "iso639_type", # (3) ISO 639 language type, see README + "script", # (4) normal scripts of expressions + "name", # (5) PanLex default name + "langvar_uid", # (6) UID of the language variety in which the default name is an expression + ], +) + + +class PanlexSwadeshCorpusReader(WordListCorpusReader): + """ + This is a class to read the PanLex Swadesh list from + + David Kamholz, Jonathan Pool, and Susan M. Colowick (2014). + PanLex: Building a Resource for Panlingual Lexical Translation. + In LREC. http://www.lrec-conf.org/proceedings/lrec2014/pdf/1029_Paper.pdf + + License: CC0 1.0 Universal + https://creativecommons.org/publicdomain/zero/1.0/legalcode + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Find the swadesh size using the fileids' path. + self.swadesh_size = re.match(r"swadesh([0-9].*)\/", self.fileids()[0]).group(1) + self._languages = {lang.panlex_uid: lang for lang in self.get_languages()} + self._macro_langauges = self.get_macrolanguages() + + def license(self): + return "CC0 1.0 Universal" + + def language_codes(self): + return self._languages.keys() + + def get_languages(self): + for line in self.raw(f"langs{self.swadesh_size}.txt").split("\n"): + if not line.strip(): # Skip empty lines. + continue + yield PanlexLanguage(*line.strip().split("\t")) + + def get_macrolanguages(self): + macro_langauges = defaultdict(list) + for lang in self._languages.values(): + macro_langauges[lang.iso639].append(lang.panlex_uid) + return macro_langauges + + def words_by_lang(self, lang_code): + """ + :return: a list of list(str) + """ + fileid = f"swadesh{self.swadesh_size}/{lang_code}.txt" + return [concept.split("\t") for concept in self.words(fileid)] + + def words_by_iso639(self, iso63_code): + """ + :return: a list of list(str) + """ + fileids = [ + f"swadesh{self.swadesh_size}/{lang_code}.txt" + for lang_code in self._macro_langauges[iso63_code] + ] + return [ + concept.split("\t") for fileid in fileids for concept in self.words(fileid) + ] + + def entries(self, fileids=None): + """ + :return: a tuple of words for the specified fileids. + """ + if not fileids: + fileids = self.fileids() + + wordlists = [self.words(f) for f in fileids] + return list(zip(*wordlists)) diff --git a/nltk/corpus/reader/pl196x.py b/nltk/corpus/reader/pl196x.py new file mode 100644 index 0000000..8d5efc0 --- /dev/null +++ b/nltk/corpus/reader/pl196x.py @@ -0,0 +1,373 @@ +# Natural Language Toolkit: +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Piotr Kasprzyk +# URL: +# For license information, see LICENSE.TXT + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.xmldocs import XMLCorpusReader + +PARA = re.compile(r"]*){0,1}>(.*?)

    ") +SENT = re.compile(r"]*){0,1}>(.*?)
    ") + +TAGGEDWORD = re.compile(r"<([wc](?: [^>]*){0,1}>)(.*?)") +WORD = re.compile(r"<[wc](?: [^>]*){0,1}>(.*?)") + +TYPE = re.compile(r'type="(.*?)"') +ANA = re.compile(r'ana="(.*?)"') + +TEXTID = re.compile(r'text id="(.*?)"') + + +class TEICorpusView(StreamBackedCorpusView): + def __init__( + self, + corpus_file, + tagged, + group_by_sent, + group_by_para, + tagset=None, + head_len=0, + textids=None, + ): + self._tagged = tagged + self._textids = textids + + self._group_by_sent = group_by_sent + self._group_by_para = group_by_para + # WARNING -- skip header + StreamBackedCorpusView.__init__(self, corpus_file, startpos=head_len) + + _pagesize = 4096 + + def read_block(self, stream): + block = stream.readlines(self._pagesize) + block = concat(block) + while (block.count(" block.count("")) or block.count( + "") + len("") + block = block[:beg] + block[beg + end :] + + output = [] + for para_str in PARA.findall(block): + para = [] + for sent_str in SENT.findall(para_str): + if not self._tagged: + sent = WORD.findall(sent_str) + else: + sent = list(map(self._parse_tag, TAGGEDWORD.findall(sent_str))) + if self._group_by_sent: + para.append(sent) + else: + para.extend(sent) + if self._group_by_para: + output.append(para) + else: + output.extend(para) + return output + + def _parse_tag(self, tag_word_tuple): + (tag, word) = tag_word_tuple + if tag.startswith("w"): + tag = ANA.search(tag).group(1) + else: # tag.startswith('c') + tag = TYPE.search(tag).group(1) + return word, tag + + +class Pl196xCorpusReader(CategorizedCorpusReader, XMLCorpusReader): + head_len = 2770 + + def __init__(self, *args, **kwargs): + if "textid_file" in kwargs: + self._textids = kwargs["textid_file"] + else: + self._textids = None + + XMLCorpusReader.__init__(self, *args) + CategorizedCorpusReader.__init__(self, kwargs) + + self._init_textids() + + def _init_textids(self): + self._f2t = defaultdict(list) + self._t2f = defaultdict(list) + if self._textids is not None: + with open(self._textids) as fp: + for line in fp: + line = line.strip() + file_id, text_ids = line.split(" ", 1) + if file_id not in self.fileids(): + raise ValueError( + "In text_id mapping file %s: %s not found" + % (self._textids, file_id) + ) + for text_id in text_ids.split(self._delimiter): + self._add_textids(file_id, text_id) + + def _add_textids(self, file_id, text_id): + self._f2t[file_id].append(text_id) + self._t2f[text_id].append(file_id) + + def _resolve(self, fileids, categories, textids=None): + tmp = None + if ( + len( + list( + filter( + lambda accessor: accessor is None, + (fileids, categories, textids), + ) + ) + ) + != 1 + ): + raise ValueError( + "Specify exactly one of: fileids, " "categories or textids" + ) + + if fileids is not None: + return fileids, None + + if categories is not None: + return self.fileids(categories), None + + if textids is not None: + if isinstance(textids, str): + textids = [textids] + files = sum((self._t2f[t] for t in textids), []) + tdict = dict() + for f in files: + tdict[f] = set(self._f2t[f]) & set(textids) + return files, tdict + + def decode_tag(self, tag): + # to be implemented + return tag + + def textids(self, fileids=None, categories=None): + """ + In the pl196x corpus each category is stored in single + file and thus both methods provide identical functionality. In order + to accommodate finer granularity, a non-standard textids() method was + implemented. All the main functions can be supplied with a list + of required chunks---giving much more control to the user. + """ + fileids, _ = self._resolve(fileids, categories) + if fileids is None: + return sorted(self._t2f) + + if isinstance(fileids, str): + fileids = [fileids] + return sorted(sum((self._f2t[d] for d in fileids), [])) + + def words(self, fileids=None, categories=None, textids=None): + fileids, textids = self._resolve(fileids, categories, textids) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + if textids: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + False, + False, + False, + head_len=self.head_len, + textids=textids[fileid], + ) + for fileid in fileids + ] + ) + else: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + False, + False, + False, + head_len=self.head_len, + ) + for fileid in fileids + ] + ) + + def sents(self, fileids=None, categories=None, textids=None): + fileids, textids = self._resolve(fileids, categories, textids) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + if textids: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + False, + True, + False, + head_len=self.head_len, + textids=textids[fileid], + ) + for fileid in fileids + ] + ) + else: + return concat( + [ + TEICorpusView( + self.abspath(fileid), False, True, False, head_len=self.head_len + ) + for fileid in fileids + ] + ) + + def paras(self, fileids=None, categories=None, textids=None): + fileids, textids = self._resolve(fileids, categories, textids) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + if textids: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + False, + True, + True, + head_len=self.head_len, + textids=textids[fileid], + ) + for fileid in fileids + ] + ) + else: + return concat( + [ + TEICorpusView( + self.abspath(fileid), False, True, True, head_len=self.head_len + ) + for fileid in fileids + ] + ) + + def tagged_words(self, fileids=None, categories=None, textids=None): + fileids, textids = self._resolve(fileids, categories, textids) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + if textids: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + True, + False, + False, + head_len=self.head_len, + textids=textids[fileid], + ) + for fileid in fileids + ] + ) + else: + return concat( + [ + TEICorpusView( + self.abspath(fileid), True, False, False, head_len=self.head_len + ) + for fileid in fileids + ] + ) + + def tagged_sents(self, fileids=None, categories=None, textids=None): + fileids, textids = self._resolve(fileids, categories, textids) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + if textids: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + True, + True, + False, + head_len=self.head_len, + textids=textids[fileid], + ) + for fileid in fileids + ] + ) + else: + return concat( + [ + TEICorpusView( + self.abspath(fileid), True, True, False, head_len=self.head_len + ) + for fileid in fileids + ] + ) + + def tagged_paras(self, fileids=None, categories=None, textids=None): + fileids, textids = self._resolve(fileids, categories, textids) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + + if textids: + return concat( + [ + TEICorpusView( + self.abspath(fileid), + True, + True, + True, + head_len=self.head_len, + textids=textids[fileid], + ) + for fileid in fileids + ] + ) + else: + return concat( + [ + TEICorpusView( + self.abspath(fileid), True, True, True, head_len=self.head_len + ) + for fileid in fileids + ] + ) + + def xml(self, fileids=None, categories=None): + fileids, _ = self._resolve(fileids, categories) + if len(fileids) == 1: + return XMLCorpusReader.xml(self, fileids[0]) + else: + raise TypeError("Expected a single file") diff --git a/nltk/corpus/reader/plaintext.py b/nltk/corpus/reader/plaintext.py new file mode 100644 index 0000000..9201f13 --- /dev/null +++ b/nltk/corpus/reader/plaintext.py @@ -0,0 +1,237 @@ +# Natural Language Toolkit: Plaintext Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# Nitin Madnani +# URL: +# For license information, see LICENSE.TXT + +""" +A reader for corpora that consist of plaintext documents. +""" + +import nltk.data +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tokenize import * + + +class PlaintextCorpusReader(CorpusReader): + """ + Reader for corpora that consist of plaintext documents. Paragraphs + are assumed to be split using blank lines. Sentences and words can + be tokenized using the default tokenizers, or by custom tokenizers + specified as parameters to the constructor. + + This corpus reader can be customized (e.g., to skip preface + sections of specific document formats) by creating a subclass and + overriding the ``CorpusView`` class variable. + """ + + CorpusView = StreamBackedCorpusView + """The corpus view class used by this reader. Subclasses of + ``PlaintextCorpusReader`` may specify alternative corpus view + classes (e.g., to skip the preface sections of documents.)""" + + def __init__( + self, + root, + fileids, + word_tokenizer=WordPunctTokenizer(), + sent_tokenizer=None, + para_block_reader=read_blankline_block, + encoding="utf8", + ): + r""" + Construct a new plaintext corpus reader for a set of documents + located at the given root directory. Example usage: + + >>> root = '/usr/local/share/nltk_data/corpora/webtext/' + >>> reader = PlaintextCorpusReader(root, '.*\.txt') # doctest: +SKIP + + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + :param word_tokenizer: Tokenizer for breaking sentences or + paragraphs into words. + :param sent_tokenizer: Tokenizer for breaking paragraphs + into words. + :param para_block_reader: The block reader used to divide the + corpus into paragraph blocks. + """ + CorpusReader.__init__(self, root, fileids, encoding) + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + self._para_block_reader = para_block_reader + + def words(self, fileids=None): + """ + :return: the given file(s) as a list of words + and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + self.CorpusView(path, self._read_word_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def sents(self, fileids=None): + """ + :return: the given file(s) as a list of + sentences or utterances, each encoded as a list of word + strings. + :rtype: list(list(str)) + """ + if self._sent_tokenizer is None: + try: + self._sent_tokenizer = PunktTokenizer() + except Exception: + raise ValueError("No sentence tokenizer for this corpus") + + return concat( + [ + self.CorpusView(path, self._read_sent_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def paras(self, fileids=None): + """ + :return: the given file(s) as a list of + paragraphs, each encoded as a list of sentences, which are + in turn encoded as lists of word strings. + :rtype: list(list(list(str))) + """ + if self._sent_tokenizer is None: + try: + self._sent_tokenizer = PunktTokenizer() + except Exception: + raise ValueError("No sentence tokenizer for this corpus") + + return concat( + [ + self.CorpusView(path, self._read_para_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def _read_word_block(self, stream): + words = [] + for i in range(20): # Read 20 lines at a time. + words.extend(self._word_tokenizer.tokenize(stream.readline())) + return words + + def _read_sent_block(self, stream): + sents = [] + for para in self._para_block_reader(stream): + sents.extend( + [ + self._word_tokenizer.tokenize(sent) + for sent in self._sent_tokenizer.tokenize(para) + ] + ) + return sents + + def _read_para_block(self, stream): + paras = [] + for para in self._para_block_reader(stream): + paras.append( + [ + self._word_tokenizer.tokenize(sent) + for sent in self._sent_tokenizer.tokenize(para) + ] + ) + return paras + + +class CategorizedPlaintextCorpusReader(CategorizedCorpusReader, PlaintextCorpusReader): + """ + A reader for plaintext corpora whose documents are divided into + categories based on their file identifiers. + """ + + def __init__(self, *args, **kwargs): + """ + Initialize the corpus reader. Categorization arguments + (``cat_pattern``, ``cat_map``, and ``cat_file``) are passed to + the ``CategorizedCorpusReader`` constructor. The remaining arguments + are passed to the ``PlaintextCorpusReader`` constructor. + """ + CategorizedCorpusReader.__init__(self, kwargs) + PlaintextCorpusReader.__init__(self, *args, **kwargs) + + +class PortugueseCategorizedPlaintextCorpusReader(CategorizedPlaintextCorpusReader): + """ + This class is identical with CategorizedPlaintextCorpusReader, + except that it initializes a Portuguese PunktTokenizer: + + >>> from nltk.corpus import machado + >>> print(machado._sent_tokenizer._lang) + portuguese + + """ + + def __init__(self, *args, **kwargs): + CategorizedPlaintextCorpusReader.__init__(self, *args, **kwargs) + # Fixed (@ekaf 2025), new way to invoke Punkt: + self._sent_tokenizer = PunktTokenizer("portuguese") + + +class EuroparlCorpusReader(PlaintextCorpusReader): + """ + Reader for Europarl corpora that consist of plaintext documents. + Documents are divided into chapters instead of paragraphs as + for regular plaintext documents. Chapters are separated using blank + lines. Everything is inherited from ``PlaintextCorpusReader`` except + that: + + - Since the corpus is pre-processed and pre-tokenized, the + word tokenizer should just split the line at whitespaces. + - For the same reason, the sentence tokenizer should just + split the paragraph at line breaks. + - There is a new 'chapters()' method that returns chapters instead + instead of paragraphs. + - The 'paras()' method inherited from PlaintextCorpusReader is + made non-functional to remove any confusion between chapters + and paragraphs for Europarl. + """ + + def _read_word_block(self, stream): + words = [] + for i in range(20): # Read 20 lines at a time. + words.extend(stream.readline().split()) + return words + + def _read_sent_block(self, stream): + sents = [] + for para in self._para_block_reader(stream): + sents.extend([sent.split() for sent in para.splitlines()]) + return sents + + def _read_para_block(self, stream): + paras = [] + for para in self._para_block_reader(stream): + paras.append([sent.split() for sent in para.splitlines()]) + return paras + + def chapters(self, fileids=None): + """ + :return: the given file(s) as a list of + chapters, each encoded as a list of sentences, which are + in turn encoded as lists of word strings. + :rtype: list(list(list(str))) + """ + return concat( + [ + self.CorpusView(fileid, self._read_para_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def paras(self, fileids=None): + raise NotImplementedError( + "The Europarl corpus reader does not support paragraphs. Please use chapters() instead." + ) diff --git a/nltk/corpus/reader/ppattach.py b/nltk/corpus/reader/ppattach.py new file mode 100644 index 0000000..857780f --- /dev/null +++ b/nltk/corpus/reader/ppattach.py @@ -0,0 +1,95 @@ +# Natural Language Toolkit: PP Attachment Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Read lines from the Prepositional Phrase Attachment Corpus. + +The PP Attachment Corpus contains several files having the format: + +sentence_id verb noun1 preposition noun2 attachment + +For example: + +42960 gives authority to administration V +46742 gives inventors of microchip N + +The PP attachment is to the verb phrase (V) or noun phrase (N), i.e.: + +(VP gives (NP authority) (PP to administration)) +(VP gives (NP inventors (PP of microchip))) + +The corpus contains the following files: + +training: training set +devset: development test set, used for algorithm development. +test: test set, used to report results +bitstrings: word classes derived from Mutual Information Clustering for the Wall Street Journal. + +Ratnaparkhi, Adwait (1994). A Maximum Entropy Model for Prepositional +Phrase Attachment. Proceedings of the ARPA Human Language Technology +Conference. [http://www.cis.upenn.edu/~adwait/papers/hlt94.ps] + +The PP Attachment Corpus is distributed with NLTK with the permission +of the author. +""" + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * + + +class PPAttachment: + def __init__(self, sent, verb, noun1, prep, noun2, attachment): + self.sent = sent + self.verb = verb + self.noun1 = noun1 + self.prep = prep + self.noun2 = noun2 + self.attachment = attachment + + def __repr__(self): + return ( + "PPAttachment(sent=%r, verb=%r, noun1=%r, prep=%r, " + "noun2=%r, attachment=%r)" + % (self.sent, self.verb, self.noun1, self.prep, self.noun2, self.attachment) + ) + + +class PPAttachmentCorpusReader(CorpusReader): + """ + sentence_id verb noun1 preposition noun2 attachment + """ + + def attachments(self, fileids): + return concat( + [ + StreamBackedCorpusView(fileid, self._read_obj_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tuples(self, fileids): + return concat( + [ + StreamBackedCorpusView(fileid, self._read_tuple_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def _read_tuple_block(self, stream): + line = stream.readline() + if line: + return [tuple(line.split())] + else: + return [] + + def _read_obj_block(self, stream): + line = stream.readline() + if line: + return [PPAttachment(*line.split())] + else: + return [] diff --git a/nltk/corpus/reader/propbank.py b/nltk/corpus/reader/propbank.py new file mode 100644 index 0000000..8341137 --- /dev/null +++ b/nltk/corpus/reader/propbank.py @@ -0,0 +1,524 @@ +# Natural Language Toolkit: PropBank Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +import re +from functools import total_ordering + +from defusedxml.ElementTree import parse as safe_parse + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.internals import raise_unorderable_types +from nltk.tree import Tree + + +class PropbankCorpusReader(CorpusReader): + """ + Corpus reader for the propbank corpus, which augments the Penn + Treebank with information about the predicate argument structure + of every verb instance. The corpus consists of two parts: the + predicate-argument annotations themselves, and a set of "frameset + files" which define the argument labels used by the annotations, + on a per-verb basis. Each "frameset file" contains one or more + predicates, such as ``'turn'`` or ``'turn_on'``, each of which is + divided into coarse-grained word senses called "rolesets". For + each "roleset", the frameset file provides descriptions of the + argument roles, along with examples. + """ + + def __init__( + self, + root, + propfile, + framefiles="", + verbsfile=None, + parse_fileid_xform=None, + parse_corpus=None, + encoding="utf8", + ): + """ + :param root: The root directory for this corpus. + :param propfile: The name of the file containing the predicate- + argument annotations (relative to ``root``). + :param framefiles: A list or regexp specifying the frameset + fileids for this corpus. + :param parse_fileid_xform: A transform that should be applied + to the fileids in this corpus. This should be a function + of one argument (a fileid) that returns a string (the new + fileid). + :param parse_corpus: The corpus containing the parse trees + corresponding to this corpus. These parse trees are + necessary to resolve the tree pointers used by propbank. + """ + # If framefiles is specified as a regexp, expand it. + if isinstance(framefiles, str): + framefiles = find_corpus_fileids(root, framefiles) + framefiles = list(framefiles) + # Initialize the corpus reader. + CorpusReader.__init__(self, root, [propfile, verbsfile] + framefiles, encoding) + + # Record our frame fileids & prop file. + self._propfile = propfile + self._framefiles = framefiles + self._verbsfile = verbsfile + self._parse_fileid_xform = parse_fileid_xform + self._parse_corpus = parse_corpus + + def instances(self, baseform=None): + """ + :return: a corpus view that acts as a list of + ``PropBankInstance`` objects, one for each noun in the corpus. + """ + kwargs = {} + if baseform is not None: + kwargs["instance_filter"] = lambda inst: inst.baseform == baseform + return StreamBackedCorpusView( + self.abspath(self._propfile), + lambda stream: self._read_instance_block(stream, **kwargs), + encoding=self.encoding(self._propfile), + ) + + def lines(self): + """ + :return: a corpus view that acts as a list of strings, one for + each line in the predicate-argument annotation file. + """ + return StreamBackedCorpusView( + self.abspath(self._propfile), + read_line_block, + encoding=self.encoding(self._propfile), + ) + + def roleset(self, roleset_id): + """ + :return: the xml description for the given roleset. + """ + baseform = roleset_id.split(".")[0] + framefile = "frames/%s.xml" % baseform + if framefile not in self._framefiles: + raise ValueError("Frameset file for %s not found" % roleset_id) + + # n.b.: The encoding for XML fileids is specified by the file + # itself; so we ignore self._encoding here. + with self.abspath(framefile).open() as fp: + etree = safe_parse(fp).getroot() + for roleset in etree.findall("predicate/roleset"): + if roleset.attrib["id"] == roleset_id: + return roleset + raise ValueError(f"Roleset {roleset_id} not found in {framefile}") + + def rolesets(self, baseform=None): + """ + :return: list of xml descriptions for rolesets. + """ + if baseform is not None: + framefile = "frames/%s.xml" % baseform + if framefile not in self._framefiles: + raise ValueError("Frameset file for %s not found" % baseform) + framefiles = [framefile] + else: + framefiles = self._framefiles + + rsets = [] + for framefile in framefiles: + # n.b.: The encoding for XML fileids is specified by the file + # itself; so we ignore self._encoding here. + with self.abspath(framefile).open() as fp: + etree = safe_parse(fp).getroot() + rsets.append(etree.findall("predicate/roleset")) + return LazyConcatenation(rsets) + + def verbs(self): + """ + :return: a corpus view that acts as a list of all verb lemmas + in this corpus (from the verbs.txt file). + """ + return StreamBackedCorpusView( + self.abspath(self._verbsfile), + read_line_block, + encoding=self.encoding(self._verbsfile), + ) + + def _read_instance_block(self, stream, instance_filter=lambda inst: True): + block = [] + + # Read 100 at a time. + for i in range(100): + line = stream.readline().strip() + if line: + inst = PropbankInstance.parse( + line, self._parse_fileid_xform, self._parse_corpus + ) + if instance_filter(inst): + block.append(inst) + + return block + + +###################################################################### +# { Propbank Instance & related datatypes +###################################################################### + + +class PropbankInstance: + def __init__( + self, + fileid, + sentnum, + wordnum, + tagger, + roleset, + inflection, + predicate, + arguments, + parse_corpus=None, + ): + self.fileid = fileid + """The name of the file containing the parse tree for this + instance's sentence.""" + + self.sentnum = sentnum + """The sentence number of this sentence within ``fileid``. + Indexing starts from zero.""" + + self.wordnum = wordnum + """The word number of this instance's predicate within its + containing sentence. Word numbers are indexed starting from + zero, and include traces and other empty parse elements.""" + + self.tagger = tagger + """An identifier for the tagger who tagged this instance; or + ``'gold'`` if this is an adjuticated instance.""" + + self.roleset = roleset + """The name of the roleset used by this instance's predicate. + Use ``propbank.roleset() `` to + look up information about the roleset.""" + + self.inflection = inflection + """A ``PropbankInflection`` object describing the inflection of + this instance's predicate.""" + + self.predicate = predicate + """A ``PropbankTreePointer`` indicating the position of this + instance's predicate within its containing sentence.""" + + self.arguments = tuple(arguments) + """A list of tuples (argloc, argid), specifying the location + and identifier for each of the predicate's argument in the + containing sentence. Argument identifiers are strings such as + ``'ARG0'`` or ``'ARGM-TMP'``. This list does *not* contain + the predicate.""" + + self.parse_corpus = parse_corpus + """A corpus reader for the parse trees corresponding to the + instances in this propbank corpus.""" + + @property + def baseform(self): + """The baseform of the predicate.""" + return self.roleset.split(".")[0] + + @property + def sensenumber(self): + """The sense number of the predicate.""" + return self.roleset.split(".")[1] + + @property + def predid(self): + """Identifier of the predicate.""" + return "rel" + + def __repr__(self): + return "".format( + self.fileid, + self.sentnum, + self.wordnum, + ) + + def __str__(self): + s = "{} {} {} {} {} {}".format( + self.fileid, + self.sentnum, + self.wordnum, + self.tagger, + self.roleset, + self.inflection, + ) + items = self.arguments + ((self.predicate, "rel"),) + for argloc, argid in sorted(items): + s += f" {argloc}-{argid}" + return s + + def _get_tree(self): + if self.parse_corpus is None: + return None + if self.fileid not in self.parse_corpus.fileids(): + return None + return self.parse_corpus.parsed_sents(self.fileid)[self.sentnum] + + tree = property( + _get_tree, + doc=""" + The parse tree corresponding to this instance, or None if + the corresponding tree is not available.""", + ) + + @staticmethod + def parse(s, parse_fileid_xform=None, parse_corpus=None): + pieces = s.split() + if len(pieces) < 7: + raise ValueError("Badly formatted propbank line: %r" % s) + + # Divide the line into its basic pieces. + (fileid, sentnum, wordnum, tagger, roleset, inflection) = pieces[:6] + rel = [p for p in pieces[6:] if p.endswith("-rel")] + args = [p for p in pieces[6:] if not p.endswith("-rel")] + if len(rel) != 1: + raise ValueError("Badly formatted propbank line: %r" % s) + + # Apply the fileid selector, if any. + if parse_fileid_xform is not None: + fileid = parse_fileid_xform(fileid) + + # Convert the numeric fields and parse the inflection, the predicate + # location, and the argument pointers. Any malformed field would + # otherwise leak a cryptic ValueError (e.g. "invalid literal for int()", + # "Bad propbank inflection string", "bad propbank pointer", or an + # unpacking error from an argument missing its "-" separator) that aborts + # iteration over the whole corpus; fail with the same clear message as + # the checks above, so callers can catch a single error for any + # malformed line. + try: + sentnum = int(sentnum) + wordnum = int(wordnum) + inflection = PropbankInflection.parse(inflection) + predicate = PropbankTreePointer.parse(rel[0][:-4]) + arguments = [] + for arg in args: + argloc, argid = arg.split("-", 1) + arguments.append((PropbankTreePointer.parse(argloc), argid)) + except ValueError: + raise ValueError("Badly formatted propbank line: %r" % s) from None + + # Put it all together. + return PropbankInstance( + fileid, + sentnum, + wordnum, + tagger, + roleset, + inflection, + predicate, + arguments, + parse_corpus, + ) + + +class PropbankPointer: + """ + A pointer used by propbank to identify one or more constituents in + a parse tree. ``PropbankPointer`` is an abstract base class with + three concrete subclasses: + + - ``PropbankTreePointer`` is used to point to single constituents. + - ``PropbankSplitTreePointer`` is used to point to 'split' + constituents, which consist of a sequence of two or more + ``PropbankTreePointer`` pointers. + - ``PropbankChainTreePointer`` is used to point to entire trace + chains in a tree. It consists of a sequence of pieces, which + can be ``PropbankTreePointer`` or ``PropbankSplitTreePointer`` pointers. + """ + + def __init__(self): + if self.__class__ == PropbankPointer: + raise NotImplementedError() + + +class PropbankChainTreePointer(PropbankPointer): + def __init__(self, pieces): + self.pieces = pieces + """A list of the pieces that make up this chain. Elements may + be either ``PropbankSplitTreePointer`` or + ``PropbankTreePointer`` pointers.""" + + def __str__(self): + return "*".join("%s" % p for p in self.pieces) + + def __repr__(self): + return "" % self + + def select(self, tree): + if tree is None: + raise ValueError("Parse tree not available") + return Tree("*CHAIN*", [p.select(tree) for p in self.pieces]) + + +class PropbankSplitTreePointer(PropbankPointer): + def __init__(self, pieces): + self.pieces = pieces + """A list of the pieces that make up this chain. Elements are + all ``PropbankTreePointer`` pointers.""" + + def __str__(self): + return ",".join("%s" % p for p in self.pieces) + + def __repr__(self): + return "" % self + + def select(self, tree): + if tree is None: + raise ValueError("Parse tree not available") + return Tree("*SPLIT*", [p.select(tree) for p in self.pieces]) + + +@total_ordering +class PropbankTreePointer(PropbankPointer): + """ + wordnum:height*wordnum:height*... + wordnum:height, + + """ + + def __init__(self, wordnum, height): + self.wordnum = wordnum + self.height = height + + @staticmethod + def parse(s): + # Deal with chains (xx*yy*zz) + pieces = s.split("*") + if len(pieces) > 1: + return PropbankChainTreePointer( + [PropbankTreePointer.parse(elt) for elt in pieces] + ) + + # Deal with split args (xx,yy,zz) + pieces = s.split(",") + if len(pieces) > 1: + return PropbankSplitTreePointer( + [PropbankTreePointer.parse(elt) for elt in pieces] + ) + + # Deal with normal pointers. + pieces = s.split(":") + if len(pieces) != 2: + raise ValueError("bad propbank pointer %r" % s) + return PropbankTreePointer(int(pieces[0]), int(pieces[1])) + + def __str__(self): + return f"{self.wordnum}:{self.height}" + + def __repr__(self): + return "PropbankTreePointer(%d, %d)" % (self.wordnum, self.height) + + def __eq__(self, other): + while isinstance(other, (PropbankChainTreePointer, PropbankSplitTreePointer)): + other = other.pieces[0] + + if not isinstance(other, PropbankTreePointer): + return self is other + + return self.wordnum == other.wordnum and self.height == other.height + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + while isinstance(other, (PropbankChainTreePointer, PropbankSplitTreePointer)): + other = other.pieces[0] + + if not isinstance(other, PropbankTreePointer): + return id(self) < id(other) + + return (self.wordnum, -self.height) < (other.wordnum, -other.height) + + def select(self, tree): + if tree is None: + raise ValueError("Parse tree not available") + return tree[self.treepos(tree)] + + def treepos(self, tree): + """ + Convert this pointer to a standard 'tree position' pointer, + given that it points to the given tree. + """ + if tree is None: + raise ValueError("Parse tree not available") + stack = [tree] + treepos = [] + + wordnum = 0 + while True: + # tree node: + if isinstance(stack[-1], Tree): + # Select the next child. + if len(treepos) < len(stack): + treepos.append(0) + else: + treepos[-1] += 1 + # Update the stack. + if treepos[-1] < len(stack[-1]): + stack.append(stack[-1][treepos[-1]]) + else: + # End of node's child list: pop up a level. + stack.pop() + treepos.pop() + # word node: + else: + if wordnum == self.wordnum: + return tuple(treepos[: len(treepos) - self.height - 1]) + else: + wordnum += 1 + stack.pop() + + +class PropbankInflection: + # { Inflection Form + INFINITIVE = "i" + GERUND = "g" + PARTICIPLE = "p" + FINITE = "v" + # { Inflection Tense + FUTURE = "f" + PAST = "p" + PRESENT = "n" + # { Inflection Aspect + PERFECT = "p" + PROGRESSIVE = "o" + PERFECT_AND_PROGRESSIVE = "b" + # { Inflection Person + THIRD_PERSON = "3" + # { Inflection Voice + ACTIVE = "a" + PASSIVE = "p" + # { Inflection + NONE = "-" + # } + + def __init__(self, form="-", tense="-", aspect="-", person="-", voice="-"): + self.form = form + self.tense = tense + self.aspect = aspect + self.person = person + self.voice = voice + + def __str__(self): + return self.form + self.tense + self.aspect + self.person + self.voice + + def __repr__(self): + return "" % self + + _VALIDATE = re.compile(r"[igpv\-][fpn\-][pob\-][3\-][ap\-]$") + + @staticmethod + def parse(s): + if not isinstance(s, str): + raise TypeError("expected a string") + if len(s) != 5 or not PropbankInflection._VALIDATE.match(s): + raise ValueError("Bad propbank inflection string %r" % s) + return PropbankInflection(*s) diff --git a/nltk/corpus/reader/pros_cons.py b/nltk/corpus/reader/pros_cons.py new file mode 100644 index 0000000..6fc3ffb --- /dev/null +++ b/nltk/corpus/reader/pros_cons.py @@ -0,0 +1,133 @@ +# Natural Language Toolkit: Pros and Cons Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +CorpusReader for the Pros and Cons dataset. + +- Pros and Cons dataset information - + +Contact: Bing Liu, liub@cs.uic.edu + https://www.cs.uic.edu/~liub + +Distributed with permission. + +Related papers: + +- Murthy Ganapathibhotla and Bing Liu. "Mining Opinions in Comparative Sentences". + Proceedings of the 22nd International Conference on Computational Linguistics + (Coling-2008), Manchester, 18-22 August, 2008. + +- Bing Liu, Minqing Hu and Junsheng Cheng. "Opinion Observer: Analyzing and Comparing + Opinions on the Web". Proceedings of the 14th international World Wide Web + conference (WWW-2005), May 10-14, 2005, in Chiba, Japan. +""" +import re + +from nltk.corpus.reader.api import * +from nltk.tokenize import * + + +class ProsConsCorpusReader(CategorizedCorpusReader, CorpusReader): + """ + Reader for the Pros and Cons sentence dataset. + + >>> from nltk.corpus import pros_cons + >>> pros_cons.sents(categories='Cons') # doctest: +NORMALIZE_WHITESPACE + [['East', 'batteries', '!', 'On', '-', 'off', 'switch', 'too', 'easy', + 'to', 'maneuver', '.'], ['Eats', '...', 'no', ',', 'GULPS', 'batteries'], + ...] + >>> pros_cons.words('IntegratedPros.txt') + ['Easy', 'to', 'use', ',', 'economical', '!', ...] + """ + + CorpusView = StreamBackedCorpusView + + def __init__( + self, + root, + fileids, + word_tokenizer=WordPunctTokenizer(), + encoding="utf8", + **kwargs + ): + """ + :param root: The root directory for the corpus. + :param fileids: a list or regexp specifying the fileids in the corpus. + :param word_tokenizer: a tokenizer for breaking sentences or paragraphs + into words. Default: `WhitespaceTokenizer` + :param encoding: the encoding that should be used to read the corpus. + :param kwargs: additional parameters passed to CategorizedCorpusReader. + """ + + CorpusReader.__init__(self, root, fileids, encoding) + CategorizedCorpusReader.__init__(self, kwargs) + self._word_tokenizer = word_tokenizer + + def sents(self, fileids=None, categories=None): + """ + Return all sentences in the corpus or in the specified files/categories. + + :param fileids: a list or regexp specifying the ids of the files whose + sentences have to be returned. + :param categories: a list specifying the categories whose sentences + have to be returned. + :return: the given file(s) as a list of sentences. Each sentence is + tokenized using the specified word_tokenizer. + :rtype: list(list(str)) + """ + fileids = self._resolve(fileids, categories) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(path, self._read_sent_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def words(self, fileids=None, categories=None): + """ + Return all words and punctuation symbols in the corpus or in the specified + files/categories. + + :param fileids: a list or regexp specifying the ids of the files whose + words have to be returned. + :param categories: a list specifying the categories whose words have + to be returned. + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + fileids = self._resolve(fileids, categories) + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(path, self._read_word_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def _read_sent_block(self, stream): + sents = [] + for i in range(20): # Read 20 lines at a time. + line = stream.readline() + if not line: + continue + sent = re.match(r"^(?!\n)\s*<(Pros|Cons)>(.*)", line) + if sent: + sents.append(self._word_tokenizer.tokenize(sent.group(2).strip())) + return sents + + def _read_word_block(self, stream): + words = [] + for sent in self._read_sent_block(stream): + words.extend(sent) + return words diff --git a/nltk/corpus/reader/reviews.py b/nltk/corpus/reader/reviews.py new file mode 100644 index 0000000..0286b04 --- /dev/null +++ b/nltk/corpus/reader/reviews.py @@ -0,0 +1,337 @@ +# Natural Language Toolkit: Product Reviews Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +CorpusReader for reviews corpora (syntax based on Customer Review Corpus). + +Customer Review Corpus information +================================== + +Annotated by: Minqing Hu and Bing Liu, 2004. + Department of Computer Science + University of Illinois at Chicago + +Contact: Bing Liu, liub@cs.uic.edu + https://www.cs.uic.edu/~liub + +Distributed with permission. + +The "product_reviews_1" and "product_reviews_2" datasets respectively contain +annotated customer reviews of 5 and 9 products from amazon.com. + +Related papers: + +- Minqing Hu and Bing Liu. "Mining and summarizing customer reviews". + Proceedings of the ACM SIGKDD International Conference on Knowledge + Discovery & Data Mining (KDD-04), 2004. + +- Minqing Hu and Bing Liu. "Mining Opinion Features in Customer Reviews". + Proceedings of Nineteeth National Conference on Artificial Intelligence + (AAAI-2004), 2004. + +- Xiaowen Ding, Bing Liu and Philip S. Yu. "A Holistic Lexicon-Based Appraoch to + Opinion Mining." Proceedings of First ACM International Conference on Web + Search and Data Mining (WSDM-2008), Feb 11-12, 2008, Stanford University, + Stanford, California, USA. + +Symbols used in the annotated reviews: + + :[t]: the title of the review: Each [t] tag starts a review. + :xxxx[+|-n]: xxxx is a product feature. + :[+n]: Positive opinion, n is the opinion strength: 3 strongest, and 1 weakest. + Note that the strength is quite subjective. + You may want ignore it, but only considering + and - + :[-n]: Negative opinion + :##: start of each sentence. Each line is a sentence. + :[u]: feature not appeared in the sentence. + :[p]: feature not appeared in the sentence. Pronoun resolution is needed. + :[s]: suggestion or recommendation. + :[cc]: comparison with a competing product from a different brand. + :[cs]: comparison with a competing product from the same brand. + +Note: Some of the files (e.g. "ipod.txt", "Canon PowerShot SD500.txt") do not + provide separation between different reviews. This is due to the fact that + the dataset was specifically designed for aspect/feature-based sentiment + analysis, for which sentence-level annotation is sufficient. For document- + level classification and analysis, this peculiarity should be taken into + consideration. +""" + +import re + +from nltk.corpus.reader.api import * +from nltk.tokenize import * + +TITLE = re.compile(r"^\[t\](.*)$") # [t] Title +# find 'feature' in feature[+3]. +# The feature label is "a word, then up to 50 single-whitespace-separated +# words". The label length is *bounded* so that re.findall() cannot rescan a +# long, bracket-less line quadratically: with the previous unbounded label +# (``(?:(?:\w+\s)+)?\w+``) a crafted corpus line such as ``"word " * 100000`` +# makes each search position consume the rest of the line, hanging the reader +# (ReDoS, CWE-1333). Real feature labels are short noun phrases, so the bound +# does not affect normal corpora. +FEATURES = re.compile(r"(\w+(?:\s\w+){0,50})\[((?:\+|\-)\d)\]") +NOTES = re.compile(r"\[(?!t)(p|u|s|cc|cs)\]") # find 'p' in camera[+2][p] +SENT = re.compile(r"##(.*)$") # find tokenized sentence + + +class Review: + """ + A Review is the main block of a ReviewsCorpusReader. + """ + + def __init__(self, title=None, review_lines=None): + """ + :param title: the title of the review. + :param review_lines: the list of the ReviewLines that belong to the Review. + """ + self.title = title + if review_lines is None: + self.review_lines = [] + else: + self.review_lines = review_lines + + def add_line(self, review_line): + """ + Add a line (ReviewLine) to the review. + + :param review_line: a ReviewLine instance that belongs to the Review. + """ + assert isinstance(review_line, ReviewLine) + self.review_lines.append(review_line) + + def features(self): + """ + Return a list of features in the review. Each feature is a tuple made of + the specific item feature and the opinion strength about that feature. + + :return: all features of the review as a list of tuples (feat, score). + :rtype: list(tuple) + """ + features = [] + for review_line in self.review_lines: + features.extend(review_line.features) + return features + + def sents(self): + """ + Return all tokenized sentences in the review. + + :return: all sentences of the review as lists of tokens. + :rtype: list(list(str)) + """ + return [review_line.sent for review_line in self.review_lines] + + def __repr__(self): + return 'Review(title="{}", review_lines={})'.format( + self.title, self.review_lines + ) + + +class ReviewLine: + """ + A ReviewLine represents a sentence of the review, together with (optional) + annotations of its features and notes about the reviewed item. + """ + + def __init__(self, sent, features=None, notes=None): + self.sent = sent + if features is None: + self.features = [] + else: + self.features = features + + if notes is None: + self.notes = [] + else: + self.notes = notes + + def __repr__(self): + return "ReviewLine(features={}, notes={}, sent={})".format( + self.features, self.notes, self.sent + ) + + +class ReviewsCorpusReader(CorpusReader): + """ + Reader for the Customer Review Data dataset by Hu, Liu (2004). + Note: we are not applying any sentence tokenization at the moment, just word + tokenization. + + >>> from nltk.corpus import product_reviews_1 + >>> camera_reviews = product_reviews_1.reviews('Canon_G3.txt') + >>> review = camera_reviews[0] + >>> review.sents()[0] # doctest: +NORMALIZE_WHITESPACE + ['i', 'recently', 'purchased', 'the', 'canon', 'powershot', 'g3', 'and', 'am', + 'extremely', 'satisfied', 'with', 'the', 'purchase', '.'] + >>> review.features() # doctest: +NORMALIZE_WHITESPACE + [('canon powershot g3', '+3'), ('use', '+2'), ('picture', '+2'), + ('picture quality', '+1'), ('picture quality', '+1'), ('camera', '+2'), + ('use', '+2'), ('feature', '+1'), ('picture quality', '+3'), ('use', '+1'), + ('option', '+1')] + + We can also reach the same information directly from the stream: + + >>> product_reviews_1.features('Canon_G3.txt') + [('canon powershot g3', '+3'), ('use', '+2'), ...] + + We can compute stats for specific product features: + + >>> n_reviews = len([(feat,score) for (feat,score) in product_reviews_1.features('Canon_G3.txt') if feat=='picture']) + >>> tot = sum([int(score) for (feat,score) in product_reviews_1.features('Canon_G3.txt') if feat=='picture']) + >>> mean = tot / n_reviews + >>> print(n_reviews, tot, mean) + 15 24 1.6 + """ + + CorpusView = StreamBackedCorpusView + + def __init__( + self, root, fileids, word_tokenizer=WordPunctTokenizer(), encoding="utf8" + ): + """ + :param root: The root directory for the corpus. + :param fileids: a list or regexp specifying the fileids in the corpus. + :param word_tokenizer: a tokenizer for breaking sentences or paragraphs + into words. Default: `WordPunctTokenizer` + :param encoding: the encoding that should be used to read the corpus. + """ + + CorpusReader.__init__(self, root, fileids, encoding) + self._word_tokenizer = word_tokenizer + self._readme = "README.txt" + + def features(self, fileids=None): + """ + Return a list of features. Each feature is a tuple made of the specific + item feature and the opinion strength about that feature. + + :param fileids: a list or regexp specifying the ids of the files whose + features have to be returned. + :return: all features for the item(s) in the given file(s). + :rtype: list(tuple) + """ + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + self.CorpusView(fileid, self._read_features, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def reviews(self, fileids=None): + """ + Return all the reviews as a list of Review objects. If `fileids` is + specified, return all the reviews from each of the specified files. + + :param fileids: a list or regexp specifying the ids of the files whose + reviews have to be returned. + :return: the given file(s) as a list of reviews. + """ + if fileids is None: + fileids = self._fileids + return concat( + [ + self.CorpusView(fileid, self._read_review_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + """ + Return all sentences in the corpus or in the specified files. + + :param fileids: a list or regexp specifying the ids of the files whose + sentences have to be returned. + :return: the given file(s) as a list of sentences, each encoded as a + list of word strings. + :rtype: list(list(str)) + """ + return concat( + [ + self.CorpusView(path, self._read_sent_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def words(self, fileids=None): + """ + Return all words and punctuation symbols in the corpus or in the specified + files. + + :param fileids: a list or regexp specifying the ids of the files whose + words have to be returned. + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + self.CorpusView(path, self._read_word_block, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def _read_features(self, stream): + features = [] + for i in range(20): + line = stream.readline() + if not line: + return features + features.extend(re.findall(FEATURES, line)) + return features + + def _read_review_block(self, stream): + while True: + line = stream.readline() + if not line: + return [] # end of file. + title_match = re.match(TITLE, line) + if title_match: + review = Review( + title=title_match.group(1).strip() + ) # We create a new review + break + + # Scan until we find another line matching the regexp, or EOF. + while True: + oldpos = stream.tell() + line = stream.readline() + # End of file: + if not line: + return [review] + # Start of a new review: backup to just before it starts, and + # return the review we've already collected. + if re.match(TITLE, line): + stream.seek(oldpos) + return [review] + # Anything else is part of the review line. + feats = re.findall(FEATURES, line) + notes = re.findall(NOTES, line) + sent = re.findall(SENT, line) + if sent: + sent = self._word_tokenizer.tokenize(sent[0]) + review_line = ReviewLine(sent=sent, features=feats, notes=notes) + review.add_line(review_line) + + def _read_sent_block(self, stream): + sents = [] + for review in self._read_review_block(stream): + sents.extend([sent for sent in review.sents()]) + return sents + + def _read_word_block(self, stream): + words = [] + for i in range(20): # Read 20 lines at a time. + line = stream.readline() + sent = re.findall(SENT, line) + if sent: + words.extend(self._word_tokenizer.tokenize(sent[0])) + return words diff --git a/nltk/corpus/reader/rte.py b/nltk/corpus/reader/rte.py new file mode 100644 index 0000000..916dacc --- /dev/null +++ b/nltk/corpus/reader/rte.py @@ -0,0 +1,146 @@ +# Natural Language Toolkit: RTE Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +""" +Corpus reader for the Recognizing Textual Entailment (RTE) Challenge Corpora. + +The files were taken from the RTE1, RTE2 and RTE3 datasets and the files +were regularized. + +Filenames are of the form rte*_dev.xml and rte*_test.xml. The latter are the +gold standard annotated files. + +Each entailment corpus is a list of 'text'/'hypothesis' pairs. The following +example is taken from RTE3:: + + + + The sale was made to pay Yukos' US$ 27.5 billion tax bill, + Yuganskneftegaz was originally sold for US$ 9.4 billion to a little known + company Baikalfinansgroup which was later bought by the Russian + state-owned oil company Rosneft . + + Baikalfinansgroup was sold to Rosneft. + + +In order to provide globally unique IDs for each pair, a new attribute +``challenge`` has been added to the root element ``entailment-corpus`` of each +file, taking values 1, 2 or 3. The GID is formatted 'm-n', where 'm' is the +challenge number and 'n' is the pair ID. +""" +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.corpus.reader.xmldocs import * + + +def norm(value_string): + """ + Normalize the string value in an RTE pair's ``value`` or ``entailment`` + attribute as an integer (1, 0). + + :param value_string: the label used to classify a text/hypothesis pair + :type value_string: str + :rtype: int + """ + + valdict = {"TRUE": 1, "FALSE": 0, "YES": 1, "NO": 0} + return valdict[value_string.upper()] + + +class RTEPair: + """ + Container for RTE text-hypothesis pairs. + + The entailment relation is signalled by the ``value`` attribute in RTE1, and by + ``entailment`` in RTE2 and RTE3. These both get mapped on to the ``entailment`` + attribute of this class. + """ + + def __init__( + self, + pair, + challenge=None, + id=None, + text=None, + hyp=None, + value=None, + task=None, + length=None, + ): + """ + :param challenge: version of the RTE challenge (i.e., RTE1, RTE2 or RTE3) + :param id: identifier for the pair + :param text: the text component of the pair + :param hyp: the hypothesis component of the pair + :param value: classification label for the pair + :param task: attribute for the particular NLP task that the data was drawn from + :param length: attribute for the length of the text of the pair + """ + self.challenge = challenge + self.id = pair.attrib["id"] + self.gid = f"{self.challenge}-{self.id}" + self.text = pair[0].text + self.hyp = pair[1].text + + if "value" in pair.attrib: + self.value = norm(pair.attrib["value"]) + elif "entailment" in pair.attrib: + self.value = norm(pair.attrib["entailment"]) + else: + self.value = value + if "task" in pair.attrib: + self.task = pair.attrib["task"] + else: + self.task = task + if "length" in pair.attrib: + self.length = pair.attrib["length"] + else: + self.length = length + + def __repr__(self): + if self.challenge: + return f"" + else: + return "" % self.id + + +class RTECorpusReader(XMLCorpusReader): + """ + Corpus reader for corpora in RTE challenges. + + This is just a wrapper around the XMLCorpusReader. See module docstring above for the expected + structure of input documents. + """ + + def _read_etree(self, doc): + """ + Map the XML input into an RTEPair. + + This uses the ``getiterator()`` method from the ElementTree package to + find all the ```` elements. + + :param doc: a parsed XML document + :rtype: list(RTEPair) + """ + try: + challenge = doc.attrib["challenge"] + except KeyError: + challenge = None + pairiter = doc.iter("pair") + return [RTEPair(pair, challenge=challenge) for pair in pairiter] + + def pairs(self, fileids): + """ + Build a list of RTEPairs from a RTE corpus. + + :param fileids: a list of RTE corpus fileids + :type: list + :rtype: list(RTEPair) + """ + if isinstance(fileids, str): + fileids = [fileids] + return concat([self._read_etree(self.xml(fileid)) for fileid in fileids]) diff --git a/nltk/corpus/reader/semcor.py b/nltk/corpus/reader/semcor.py new file mode 100644 index 0000000..f2af284 --- /dev/null +++ b/nltk/corpus/reader/semcor.py @@ -0,0 +1,299 @@ +# Natural Language Toolkit: SemCor Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Nathan Schneider +# URL: +# For license information, see LICENSE.TXT + +""" +Corpus reader for the SemCor Corpus. +""" + +__docformat__ = "epytext en" + +from defusedxml.ElementTree import parse as safe_parse + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.xmldocs import XMLCorpusReader, XMLCorpusView +from nltk.tree import Tree + + +class SemcorCorpusReader(XMLCorpusReader): + """ + Corpus reader for the SemCor Corpus. + For access to the complete XML data structure, use the ``xml()`` + method. For access to simple word lists and tagged word lists, use + ``words()``, ``sents()``, ``tagged_words()``, and ``tagged_sents()``. + """ + + def __init__(self, root, fileids, wordnet, lazy=True): + XMLCorpusReader.__init__(self, root, fileids) + self._lazy = lazy + self._wordnet = wordnet + + def words(self, fileids=None): + """ + :return: the given file(s) as a list of words and punctuation symbols. + :rtype: list(str) + """ + return self._items(fileids, "word", False, False, False) + + def chunks(self, fileids=None): + """ + :return: the given file(s) as a list of chunks, + each of which is a list of words and punctuation symbols + that form a unit. + :rtype: list(list(str)) + """ + return self._items(fileids, "chunk", False, False, False) + + def tagged_chunks(self, fileids=None, tag=("pos" or "sem" or "both")): + """ + :return: the given file(s) as a list of tagged chunks, represented + in tree form. + :rtype: list(Tree) + + :param tag: `'pos'` (part of speech), `'sem'` (semantic), or `'both'` + to indicate the kind of tags to include. Semantic tags consist of + WordNet lemma IDs, plus an `'NE'` node if the chunk is a named entity + without a specific entry in WordNet. (Named entities of type 'other' + have no lemma. Other chunks not in WordNet have no semantic tag. + Punctuation tokens have `None` for their part of speech tag.) + """ + return self._items(fileids, "chunk", False, tag != "sem", tag != "pos") + + def sents(self, fileids=None): + """ + :return: the given file(s) as a list of sentences, each encoded + as a list of word strings. + :rtype: list(list(str)) + """ + return self._items(fileids, "word", True, False, False) + + def chunk_sents(self, fileids=None): + """ + :return: the given file(s) as a list of sentences, each encoded + as a list of chunks. + :rtype: list(list(list(str))) + """ + return self._items(fileids, "chunk", True, False, False) + + def tagged_sents(self, fileids=None, tag=("pos" or "sem" or "both")): + """ + :return: the given file(s) as a list of sentences. Each sentence + is represented as a list of tagged chunks (in tree form). + :rtype: list(list(Tree)) + + :param tag: `'pos'` (part of speech), `'sem'` (semantic), or `'both'` + to indicate the kind of tags to include. Semantic tags consist of + WordNet lemma IDs, plus an `'NE'` node if the chunk is a named entity + without a specific entry in WordNet. (Named entities of type 'other' + have no lemma. Other chunks not in WordNet have no semantic tag. + Punctuation tokens have `None` for their part of speech tag.) + """ + return self._items(fileids, "chunk", True, tag != "sem", tag != "pos") + + def _items(self, fileids, unit, bracket_sent, pos_tag, sem_tag): + if unit == "word" and not bracket_sent: + # the result of the SemcorWordView may be a multiword unit, so the + # LazyConcatenation will make sure the sentence is flattened + _ = lambda *args: LazyConcatenation( + (SemcorWordView if self._lazy else self._words)(*args) + ) + else: + _ = SemcorWordView if self._lazy else self._words + return concat( + [ + _(fileid, unit, bracket_sent, pos_tag, sem_tag, self._wordnet) + for fileid in self.abspaths(fileids) + ] + ) + + def _words(self, fileid, unit, bracket_sent, pos_tag, sem_tag): + """ + Helper used to implement the view methods -- returns a list of + tokens, (segmented) words, chunks, or sentences. The tokens + and chunks may optionally be tagged (with POS and sense + information). + + :param fileid: The name of the underlying file. + :param unit: One of `'token'`, `'word'`, or `'chunk'`. + :param bracket_sent: If true, include sentence bracketing. + :param pos_tag: Whether to include part-of-speech tags. + :param sem_tag: Whether to include semantic tags, namely WordNet lemma + and OOV named entity status. + """ + assert unit in ("token", "word", "chunk") + result = [] + + with fileid.open() as fp: + xmldoc = safe_parse(fp).getroot() + for xmlsent in xmldoc.findall(".//s"): + sent = [] + for xmlword in _all_xmlwords_in(xmlsent): + itm = SemcorCorpusReader._word( + xmlword, unit, pos_tag, sem_tag, self._wordnet + ) + if unit == "word": + sent.extend(itm) + else: + sent.append(itm) + + if bracket_sent: + result.append(SemcorSentence(xmlsent.attrib["snum"], sent)) + else: + result.extend(sent) + + assert None not in result + return result + + @staticmethod + def _word(xmlword, unit, pos_tag, sem_tag, wordnet): + tkn = xmlword.text + if not tkn: + tkn = "" # fixes issue 337? + + lemma = xmlword.get("lemma", tkn) # lemma or NE class + lexsn = xmlword.get("lexsn") # lex_sense (locator for the lemma's sense) + if lexsn is not None: + sense_key = lemma + "%" + lexsn + wnpos = ("n", "v", "a", "r", "s")[ + int(lexsn.split(":")[0]) - 1 + ] # see http://wordnet.princeton.edu/man/senseidx.5WN.html + else: + sense_key = wnpos = None + redef = xmlword.get( + "rdf", tkn + ) # redefinition--this indicates the lookup string + # does not exactly match the enclosed string, e.g. due to typographical adjustments + # or discontinuity of a multiword expression. If a redefinition has occurred, + # the "rdf" attribute holds its inflected form and "lemma" holds its lemma. + # For NEs, "rdf", "lemma", and "pn" all hold the same value (the NE class). + sensenum = xmlword.get("wnsn") # WordNet sense number + isOOVEntity = "pn" in xmlword.keys() # a "personal name" (NE) not in WordNet + pos = xmlword.get( + "pos" + ) # part of speech for the whole chunk (None for punctuation) + + if unit == "token": + if not pos_tag and not sem_tag: + itm = tkn + else: + itm = ( + (tkn,) + + ((pos,) if pos_tag else ()) + + ((lemma, wnpos, sensenum, isOOVEntity) if sem_tag else ()) + ) + return itm + else: + ww = tkn.split("_") # TODO: case where punctuation intervenes in MWE + if unit == "word": + return ww + else: + if sensenum is not None: + try: + sense = wordnet.lemma_from_key(sense_key) # Lemma object + except Exception: + # cannot retrieve the wordnet.Lemma object. possible reasons: + # (a) the wordnet corpus is not downloaded; + # (b) a nonexistent sense is annotated: e.g., such.s.00 triggers: + # nltk.corpus.reader.wordnet.WordNetError: No synset found for key u'such%5:00:01:specified:00' + # solution: just use the lemma name as a string + try: + sense = "%s.%s.%02d" % ( + lemma, + wnpos, + int(sensenum), + ) # e.g.: reach.v.02 + except ValueError: + sense = ( + lemma + "." + wnpos + "." + sensenum + ) # e.g. the sense number may be "2;1" + + bottom = [Tree(pos, ww)] if pos_tag else ww + + if sem_tag and isOOVEntity: + if sensenum is not None: + return Tree(sense, [Tree("NE", bottom)]) + else: # 'other' NE + return Tree("NE", bottom) + elif sem_tag and sensenum is not None: + return Tree(sense, bottom) + elif pos_tag: + return bottom[0] + else: + return bottom # chunk as a list + + +def _all_xmlwords_in(elt, result=None): + if result is None: + result = [] + for child in elt: + if child.tag in ("wf", "punc"): + result.append(child) + else: + _all_xmlwords_in(child, result) + return result + + +class SemcorSentence(list): + """ + A list of words, augmented by an attribute ``num`` used to record + the sentence identifier (the ``n`` attribute from the XML). + """ + + def __init__(self, num, items): + self.num = num + list.__init__(self, items) + + +class SemcorWordView(XMLCorpusView): + """ + A stream backed corpus view specialized for use with the BNC corpus. + """ + + def __init__(self, fileid, unit, bracket_sent, pos_tag, sem_tag, wordnet): + """ + :param fileid: The name of the underlying file. + :param unit: One of `'token'`, `'word'`, or `'chunk'`. + :param bracket_sent: If true, include sentence bracketing. + :param pos_tag: Whether to include part-of-speech tags. + :param sem_tag: Whether to include semantic tags, namely WordNet lemma + and OOV named entity status. + """ + if bracket_sent: + tagspec = ".*/s" + else: + tagspec = ".*/s/(punc|wf)" + + self._unit = unit + self._sent = bracket_sent + self._pos_tag = pos_tag + self._sem_tag = sem_tag + self._wordnet = wordnet + + XMLCorpusView.__init__(self, fileid, tagspec) + + def handle_elt(self, elt, context): + if self._sent: + return self.handle_sent(elt) + else: + return self.handle_word(elt) + + def handle_word(self, elt): + return SemcorCorpusReader._word( + elt, self._unit, self._pos_tag, self._sem_tag, self._wordnet + ) + + def handle_sent(self, elt): + sent = [] + for child in elt: + if child.tag in ("wf", "punc"): + itm = self.handle_word(child) + if self._unit == "word": + sent.extend(itm) + else: + sent.append(itm) + else: + raise ValueError("Unexpected element %s" % child.tag) + return SemcorSentence(elt.attrib["snum"], sent) diff --git a/nltk/corpus/reader/senseval.py b/nltk/corpus/reader/senseval.py new file mode 100644 index 0000000..cb584d8 --- /dev/null +++ b/nltk/corpus/reader/senseval.py @@ -0,0 +1,205 @@ +# Natural Language Toolkit: Senseval 2 Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Steven Bird (modifications) +# URL: +# For license information, see LICENSE.TXT + +""" +Read from the Senseval 2 Corpus. + +SENSEVAL [http://www.senseval.org/] +Evaluation exercises for Word Sense Disambiguation. +Organized by ACL-SIGLEX [https://www.siglex.org/] + +Prepared by Ted Pedersen , University of Minnesota, +https://www.d.umn.edu/~tpederse/data.html +Distributed with permission. + +The NLTK version of the Senseval 2 files uses well-formed XML. +Each instance of the ambiguous words "hard", "interest", "line", and "serve" +is tagged with a sense identifier, and supplied with context. +""" + +import re + +import regex +from defusedxml.ElementTree import fromstring as safe_fromstring + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tokenize import * + + +class SensevalInstance: + def __init__(self, word, position, context, senses): + self.word = word + self.senses = tuple(senses) + self.position = position + self.context = context + + def __repr__(self): + return "SensevalInstance(word=%r, position=%r, " "context=%r, senses=%r)" % ( + self.word, + self.position, + self.context, + self.senses, + ) + + +class SensevalCorpusReader(CorpusReader): + def instances(self, fileids=None): + return concat( + [ + SensevalCorpusView(fileid, enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def _entry(self, tree): + elts = [] + for lexelt in tree.findall("lexelt"): + for inst in lexelt.findall("instance"): + sense = inst[0].attrib["senseid"] + context = [(w.text, w.attrib["pos"]) for w in inst[1]] + elts.append((sense, context)) + return elts + + +class SensevalCorpusView(StreamBackedCorpusView): + def __init__(self, fileid, encoding): + StreamBackedCorpusView.__init__(self, fileid, encoding=encoding) + + self._word_tokenizer = WhitespaceTokenizer() + self._lexelt_starts = [0] # list of streampos + self._lexelts = [None] # list of lexelt names + + def read_block(self, stream): + # Decide which lexical element we're in. + lexelt_num = bisect.bisect_right(self._lexelt_starts, stream.tell()) - 1 + lexelt = self._lexelts[lexelt_num] + + instance_lines = [] + in_instance = False + while True: + line = stream.readline() + if line == "": + assert instance_lines == [] + return [] + + # Start of a lexical element? + if line.lstrip().startswith(" has no 'item=...' + lexelt = m.group(1)[1:-1] + if lexelt_num < len(self._lexelts): + assert lexelt == self._lexelts[lexelt_num] + else: + self._lexelts.append(lexelt) + self._lexelt_starts.append(stream.tell()) + + # Start of an instance? + if line.lstrip().startswith("" + elif cword.tag == "wf": + context.append((cword.text, cword.attrib["pos"])) + elif cword.tag == "s": + pass # Sentence boundary marker. + + else: + print("ACK", cword.tag) + assert False, "expected CDATA or or " + if cword.tail: + context += self._word_tokenizer.tokenize(cword.tail) + else: + assert False, "unexpected tag %s" % child.tag + return SensevalInstance(lexelt, position, context, senses) + + +def _fixXML(text): + """ + Fix the various issues with Senseval pseudo-XML. + """ + # <~> or <^> => ~ or ^ + text = re.sub(r"<([~\^])>", r"\1", text) + # fix lone & + text = re.sub(r"(\s+)\&(\s+)", r"\1&\2", text) + # fix """ + text = re.sub(r'"""', "'\"'", text) + # fix => + text = re.sub(r'(<[^<]*snum=)([^">]+)>', r'\1"\2"/>', text) + # fix foreign word tag + text = re.sub(r"<\&frasl>\s*]*>", "FRASL", text) + # remove <&I .> + text = re.sub(r"<\&I[^>]*>", "", text) + # fix <{word}> + text = re.sub(r"<{([^}]+)}>", r"\1", text) + # remove <@>,

    ,

    + text = re.sub(r"<(@|/?p)>", r"", text) + # remove <&M .> and <&T .> and <&Ms .> + text = re.sub(r"<&\w+ \.>", r"", text) + # remove lines + text = re.sub(r"]*>", r"", text) + # remove <[hi]> and <[/p]> etc + text = re.sub(r"<\[\/?[^>]+\]*>", r"", text) + # take the thing out of the brackets: <…> + text = re.sub(r"<(\&\w+;)>", r"\1", text) + # and remove the & for those patterns that aren't regular XML + text = re.sub(r"&(?!amp|gt|lt|apos|quot)", r"", text) + # fix 'abc ' style tags - now abc + # + # Possessive quantifiers (regex module) prevent catastrophic backtracking + # (ReDoS, CWE-1333): with the plain re patterns, the lazy/greedy whitespace + # and token runs rescan a long token / whitespace run that lacks the trailing + # tag quadratically. The token class [^<>\s] cannot cross the + # surrounding separators and \s cannot cross the literal '"', so making each + # run possessive is match-for-match identical while making the scan linear. + text = regex.sub( + r'[ \t]*+([^<>\s]++)[ \t]*+', r' \1', text + ) + text = regex.sub(r"\s*+\"\s*+", " \"", text) + return text diff --git a/nltk/corpus/reader/sentiwordnet.py b/nltk/corpus/reader/sentiwordnet.py new file mode 100644 index 0000000..effeb75 --- /dev/null +++ b/nltk/corpus/reader/sentiwordnet.py @@ -0,0 +1,136 @@ +# Natural Language Toolkit: SentiWordNet +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Christopher Potts +# URL: +# For license information, see LICENSE.TXT + +""" +An NLTK interface for SentiWordNet + +SentiWordNet is a lexical resource for opinion mining. +SentiWordNet assigns to each synset of WordNet three +sentiment scores: positivity, negativity, and objectivity. + +For details about SentiWordNet see: +http://sentiwordnet.isti.cnr.it/ + + >>> from nltk.corpus import sentiwordnet as swn + >>> print(swn.senti_synset('breakdown.n.03')) + + >>> list(swn.senti_synsets('slow')) + [SentiSynset('decelerate.v.01'), SentiSynset('slow.v.02'),\ + SentiSynset('slow.v.03'), SentiSynset('slow.a.01'),\ + SentiSynset('slow.a.02'), SentiSynset('dense.s.04'),\ + SentiSynset('slow.a.04'), SentiSynset('boring.s.01'),\ + SentiSynset('dull.s.05'), SentiSynset('slowly.r.01'),\ + SentiSynset('behind.r.03')] + >>> happy = swn.senti_synsets('happy', 'a') + >>> happy0 = list(happy)[0] + >>> happy0.pos_score() + 0.875 + >>> happy0.neg_score() + 0.0 + >>> happy0.obj_score() + 0.125 +""" + +import re + +from nltk.corpus.reader import CorpusReader + + +class SentiWordNetCorpusReader(CorpusReader): + def __init__(self, root, fileids, encoding="utf-8"): + """ + Construct a new SentiWordNet Corpus Reader, using data from + the specified file. + """ + super().__init__(root, fileids, encoding=encoding) + if len(self._fileids) != 1: + raise ValueError("Exactly one file must be specified") + self._db = {} + self._parse_src_file() + + def _parse_src_file(self): + lines = self.open(self._fileids[0]).read().splitlines() + lines = filter((lambda x: not re.search(r"^\s*#", x)), lines) + for i, line in enumerate(lines): + fields = [field.strip() for field in re.split(r"\t+", line)] + try: + pos, offset, pos_score, neg_score, synset_terms, gloss = fields + except BaseException as e: + raise ValueError(f"Line {i} formatted incorrectly: {line}\n") from e + if pos and offset: + offset = int(offset) + self._db[(pos, offset)] = (float(pos_score), float(neg_score)) + + def senti_synset(self, *vals): + from nltk.corpus import wordnet as wn + + if tuple(vals) in self._db: + pos_score, neg_score = self._db[tuple(vals)] + pos, offset = vals + if pos == "s": + pos = "a" + synset = wn.synset_from_pos_and_offset(pos, offset) + return SentiSynset(pos_score, neg_score, synset) + else: + synset = wn.synset(vals[0]) + pos = synset.pos() + if pos == "s": + pos = "a" + offset = synset.offset() + if (pos, offset) in self._db: + pos_score, neg_score = self._db[(pos, offset)] + return SentiSynset(pos_score, neg_score, synset) + else: + return None + + def senti_synsets(self, string, pos=None): + from nltk.corpus import wordnet as wn + + sentis = [] + synset_list = wn.synsets(string, pos) + for synset in synset_list: + sentis.append(self.senti_synset(synset.name())) + sentis = filter(lambda x: x, sentis) + return sentis + + def all_senti_synsets(self): + from nltk.corpus import wordnet as wn + + for key, fields in self._db.items(): + pos, offset = key + pos_score, neg_score = fields + synset = wn.synset_from_pos_and_offset(pos, offset) + yield SentiSynset(pos_score, neg_score, synset) + + +class SentiSynset: + def __init__(self, pos_score, neg_score, synset): + self._pos_score = pos_score + self._neg_score = neg_score + self._obj_score = 1.0 - (self._pos_score + self._neg_score) + self.synset = synset + + def pos_score(self): + return self._pos_score + + def neg_score(self): + return self._neg_score + + def obj_score(self): + return self._obj_score + + def __str__(self): + """Prints just the Pos/Neg scores for now.""" + s = "<" + s += self.synset.name() + ": " + s += "PosScore=%s " % self._pos_score + s += "NegScore=%s" % self._neg_score + s += ">" + return s + + def __repr__(self): + return "Senti" + repr(self.synset) diff --git a/nltk/corpus/reader/sinica_treebank.py b/nltk/corpus/reader/sinica_treebank.py new file mode 100644 index 0000000..aecaeb2 --- /dev/null +++ b/nltk/corpus/reader/sinica_treebank.py @@ -0,0 +1,75 @@ +# Natural Language Toolkit: Sinica Treebank Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Sinica Treebank Corpus Sample + +http://rocling.iis.sinica.edu.tw/CKIP/engversion/treebank.htm + +10,000 parsed sentences, drawn from the Academia Sinica Balanced +Corpus of Modern Chinese. Parse tree notation is based on +Information-based Case Grammar. Tagset documentation is available +at https://www.sinica.edu.tw/SinicaCorpus/modern_e_wordtype.html + +Language and Knowledge Processing Group, Institute of Information +Science, Academia Sinica + +The data is distributed with the Natural Language Toolkit under the terms of +the Creative Commons Attribution-NonCommercial-ShareAlike License +[https://creativecommons.org/licenses/by-nc-sa/2.5/]. + +References: + +Feng-Yi Chen, Pi-Fang Tsai, Keh-Jiann Chen, and Chu-Ren Huang (1999) +The Construction of Sinica Treebank. Computational Linguistics and +Chinese Language Processing, 4, pp 87-104. + +Huang Chu-Ren, Keh-Jiann Chen, Feng-Yi Chen, Keh-Jiann Chen, Zhao-Ming +Gao, and Kuang-Yu Chen. 2000. Sinica Treebank: Design Criteria, +Annotation Guidelines, and On-line Interface. Proceedings of 2nd +Chinese Language Processing Workshop, Association for Computational +Linguistics. + +Chen Keh-Jiann and Yu-Ming Hsieh (2004) Chinese Treebanks and Grammar +Extraction, Proceedings of IJCNLP-04, pp560-565. +""" + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tag import map_tag +from nltk.tree import sinica_parse + +IDENTIFIER = re.compile(r"^#\S+\s") +APPENDIX = re.compile(r"(?<=\))#.*$") +TAGWORD = re.compile(r":([^:()|]+):([^:()|]+)") +WORD = re.compile(r":[^:()|]+:([^:()|]+)") + + +class SinicaTreebankCorpusReader(SyntaxCorpusReader): + """ + Reader for the sinica treebank. + """ + + def _read_block(self, stream): + sent = stream.readline() + sent = IDENTIFIER.sub("", sent) + sent = APPENDIX.sub("", sent) + return [sent] + + def _parse(self, sent): + return sinica_parse(sent) + + def _tag(self, sent, tagset=None): + tagged_sent = [(w, t) for (t, w) in TAGWORD.findall(sent)] + if tagset and tagset != self._tagset: + tagged_sent = [ + (w, map_tag(self._tagset, tagset, t)) for (w, t) in tagged_sent + ] + return tagged_sent + + def _word(self, sent): + return WORD.findall(sent) diff --git a/nltk/corpus/reader/string_category.py b/nltk/corpus/reader/string_category.py new file mode 100644 index 0000000..0c753b9 --- /dev/null +++ b/nltk/corpus/reader/string_category.py @@ -0,0 +1,56 @@ +# Natural Language Toolkit: String Category Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Read tuples from a corpus consisting of categorized strings. +For example, from the question classification corpus: + +NUM:dist How far is it from Denver to Aspen ? +LOC:city What county is Modesto , California in ? +HUM:desc Who was Galileo ? +DESC:def What is an atom ? +NUM:date When did Hawaii become a state ? +""" + +from nltk.corpus.reader.api import * + +# based on PPAttachmentCorpusReader +from nltk.corpus.reader.util import * + + +# [xx] Should the order of the tuple be reversed -- in most other places +# in nltk, we use the form (data, tag) -- e.g., tagged words and +# labeled texts for classifiers. +class StringCategoryCorpusReader(CorpusReader): + def __init__(self, root, fileids, delimiter=" ", encoding="utf8"): + """ + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + :param delimiter: Field delimiter + """ + CorpusReader.__init__(self, root, fileids, encoding) + self._delimiter = delimiter + + def tuples(self, fileids=None): + if fileids is None: + fileids = self._fileids + elif isinstance(fileids, str): + fileids = [fileids] + return concat( + [ + StreamBackedCorpusView(fileid, self._read_tuple_block, encoding=enc) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def _read_tuple_block(self, stream): + line = stream.readline().strip() + if line: + return [tuple(line.split(self._delimiter, 1))] + else: + return [] diff --git a/nltk/corpus/reader/switchboard.py b/nltk/corpus/reader/switchboard.py new file mode 100644 index 0000000..30b26da --- /dev/null +++ b/nltk/corpus/reader/switchboard.py @@ -0,0 +1,125 @@ +# Natural Language Toolkit: Switchboard Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT +import re + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tag import map_tag, str2tuple + + +class SwitchboardTurn(list): + """ + A specialized list object used to encode switchboard utterances. + The elements of the list are the words in the utterance; and two + attributes, ``speaker`` and ``id``, are provided to retrieve the + spearker identifier and utterance id. Note that utterance ids + are only unique within a given discourse. + """ + + def __init__(self, words, speaker, id): + list.__init__(self, words) + self.speaker = speaker + self.id = int(id) + + def __repr__(self): + if len(self) == 0: + text = "" + elif isinstance(self[0], tuple): + text = " ".join("%s/%s" % w for w in self) + else: + text = " ".join(self) + return f"<{self.speaker}.{self.id}: {text!r}>" + + +class SwitchboardCorpusReader(CorpusReader): + _FILES = ["tagged"] + # Use the "tagged" file even for non-tagged data methods, since + # it's tokenized. + + def __init__(self, root, tagset=None): + CorpusReader.__init__(self, root, self._FILES) + self._tagset = tagset + + def words(self): + return StreamBackedCorpusView(self.abspath("tagged"), self._words_block_reader) + + def tagged_words(self, tagset=None): + def tagged_words_block_reader(stream): + return self._tagged_words_block_reader(stream, tagset) + + return StreamBackedCorpusView(self.abspath("tagged"), tagged_words_block_reader) + + def turns(self): + return StreamBackedCorpusView(self.abspath("tagged"), self._turns_block_reader) + + def tagged_turns(self, tagset=None): + def tagged_turns_block_reader(stream): + return self._tagged_turns_block_reader(stream, tagset) + + return StreamBackedCorpusView(self.abspath("tagged"), tagged_turns_block_reader) + + def discourses(self): + return StreamBackedCorpusView( + self.abspath("tagged"), self._discourses_block_reader + ) + + def tagged_discourses(self, tagset=False): + def tagged_discourses_block_reader(stream): + return self._tagged_discourses_block_reader(stream, tagset) + + return StreamBackedCorpusView( + self.abspath("tagged"), tagged_discourses_block_reader + ) + + def _discourses_block_reader(self, stream): + # returns at most 1 discourse. (The other methods depend on this.) + return [ + [ + self._parse_utterance(u, include_tag=False) + for b in read_blankline_block(stream) + for u in b.split("\n") + if u.strip() + ] + ] + + def _tagged_discourses_block_reader(self, stream, tagset=None): + # returns at most 1 discourse. (The other methods depend on this.) + return [ + [ + self._parse_utterance(u, include_tag=True, tagset=tagset) + for b in read_blankline_block(stream) + for u in b.split("\n") + if u.strip() + ] + ] + + def _turns_block_reader(self, stream): + return self._discourses_block_reader(stream)[0] + + def _tagged_turns_block_reader(self, stream, tagset=None): + return self._tagged_discourses_block_reader(stream, tagset)[0] + + def _words_block_reader(self, stream): + return sum(self._discourses_block_reader(stream)[0], []) + + def _tagged_words_block_reader(self, stream, tagset=None): + return sum(self._tagged_discourses_block_reader(stream, tagset)[0], []) + + _UTTERANCE_RE = re.compile(r"(\w+)\.(\d+)\:\s*(.*)") + _SEP = "/" + + def _parse_utterance(self, utterance, include_tag, tagset=None): + m = self._UTTERANCE_RE.match(utterance) + if m is None: + raise ValueError("Bad utterance %r" % utterance) + speaker, id, text = m.groups() + words = [str2tuple(s, self._SEP) for s in text.split()] + if not include_tag: + words = [w for (w, t) in words] + elif tagset and tagset != self._tagset: + words = [(w, map_tag(self._tagset, tagset, t)) for (w, t) in words] + return SwitchboardTurn(words, speaker, id) diff --git a/nltk/corpus/reader/tagged.py b/nltk/corpus/reader/tagged.py new file mode 100644 index 0000000..820098c --- /dev/null +++ b/nltk/corpus/reader/tagged.py @@ -0,0 +1,354 @@ +# Natural Language Toolkit: Tagged Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# Jacob Perkins +# URL: +# For license information, see LICENSE.TXT + +""" +A reader for corpora whose documents contain part-of-speech-tagged words. +""" + +import os + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.timit import read_timit_block +from nltk.corpus.reader.util import * +from nltk.tag import map_tag, str2tuple +from nltk.tokenize import * + + +class TaggedCorpusReader(CorpusReader): + """ + Reader for simple part-of-speech tagged corpora. Paragraphs are + assumed to be split using blank lines. Sentences and words can be + tokenized using the default tokenizers, or by custom tokenizers + specified as parameters to the constructor. Words are parsed + using ``nltk.tag.str2tuple``. By default, ``'/'`` is used as the + separator. I.e., words should have the form:: + + word1/tag1 word2/tag2 word3/tag3 ... + + But custom separators may be specified as parameters to the + constructor. Part of speech tags are case-normalized to upper + case. + """ + + def __init__( + self, + root, + fileids, + sep="/", + word_tokenizer=WhitespaceTokenizer(), + sent_tokenizer=RegexpTokenizer("\n", gaps=True), + para_block_reader=read_blankline_block, + encoding="utf8", + tagset=None, + ): + """ + Construct a new Tagged Corpus reader for a set of documents + located at the given root directory. Example usage: + + >>> root = '/...path to corpus.../' + >>> reader = TaggedCorpusReader(root, '.*', '.txt') # doctest: +SKIP + + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + """ + CorpusReader.__init__(self, root, fileids, encoding) + self._sep = sep + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + self._para_block_reader = para_block_reader + self._tagset = tagset + + def words(self, fileids=None): + """ + :return: the given file(s) as a list of words + and punctuation symbols. + :rtype: list(str) + """ + return concat( + [ + TaggedCorpusView( + fileid, + enc, + False, + False, + False, + self._sep, + self._word_tokenizer, + self._sent_tokenizer, + self._para_block_reader, + None, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def sents(self, fileids=None): + """ + :return: the given file(s) as a list of + sentences or utterances, each encoded as a list of word + strings. + :rtype: list(list(str)) + """ + return concat( + [ + TaggedCorpusView( + fileid, + enc, + False, + True, + False, + self._sep, + self._word_tokenizer, + self._sent_tokenizer, + self._para_block_reader, + None, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def paras(self, fileids=None): + """ + :return: the given file(s) as a list of + paragraphs, each encoded as a list of sentences, which are + in turn encoded as lists of word strings. + :rtype: list(list(list(str))) + """ + return concat( + [ + TaggedCorpusView( + fileid, + enc, + False, + True, + True, + self._sep, + self._word_tokenizer, + self._sent_tokenizer, + self._para_block_reader, + None, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_words(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of tagged + words and punctuation symbols, encoded as tuples + ``(word,tag)``. + :rtype: list(tuple(str,str)) + """ + if tagset and tagset != self._tagset: + tag_mapping_function = lambda t: map_tag(self._tagset, tagset, t) + else: + tag_mapping_function = None + return concat( + [ + TaggedCorpusView( + fileid, + enc, + True, + False, + False, + self._sep, + self._word_tokenizer, + self._sent_tokenizer, + self._para_block_reader, + tag_mapping_function, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_sents(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of + sentences, each encoded as a list of ``(word,tag)`` tuples. + + :rtype: list(list(tuple(str,str))) + """ + if tagset and tagset != self._tagset: + tag_mapping_function = lambda t: map_tag(self._tagset, tagset, t) + else: + tag_mapping_function = None + return concat( + [ + TaggedCorpusView( + fileid, + enc, + True, + True, + False, + self._sep, + self._word_tokenizer, + self._sent_tokenizer, + self._para_block_reader, + tag_mapping_function, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + def tagged_paras(self, fileids=None, tagset=None): + """ + :return: the given file(s) as a list of + paragraphs, each encoded as a list of sentences, which are + in turn encoded as lists of ``(word,tag)`` tuples. + :rtype: list(list(list(tuple(str,str)))) + """ + if tagset and tagset != self._tagset: + tag_mapping_function = lambda t: map_tag(self._tagset, tagset, t) + else: + tag_mapping_function = None + return concat( + [ + TaggedCorpusView( + fileid, + enc, + True, + True, + True, + self._sep, + self._word_tokenizer, + self._sent_tokenizer, + self._para_block_reader, + tag_mapping_function, + ) + for (fileid, enc) in self.abspaths(fileids, True) + ] + ) + + +class CategorizedTaggedCorpusReader(CategorizedCorpusReader, TaggedCorpusReader): + """ + A reader for part-of-speech tagged corpora whose documents are + divided into categories based on their file identifiers. + """ + + def __init__(self, *args, **kwargs): + """ + Initialize the corpus reader. Categorization arguments + (``cat_pattern``, ``cat_map``, and ``cat_file``) are passed to + the ``CategorizedCorpusReader`` constructor. The remaining arguments + are passed to the ``TaggedCorpusReader``. + """ + CategorizedCorpusReader.__init__(self, kwargs) + TaggedCorpusReader.__init__(self, *args, **kwargs) + + def tagged_words(self, fileids=None, categories=None, tagset=None): + return super().tagged_words(self._resolve(fileids, categories), tagset) + + def tagged_sents(self, fileids=None, categories=None, tagset=None): + return super().tagged_sents(self._resolve(fileids, categories), tagset) + + def tagged_paras(self, fileids=None, categories=None, tagset=None): + return super().tagged_paras(self._resolve(fileids, categories), tagset) + + +class TaggedCorpusView(StreamBackedCorpusView): + """ + A specialized corpus view for tagged documents. It can be + customized via flags to divide the tagged corpus documents up by + sentence or paragraph, and to include or omit part of speech tags. + ``TaggedCorpusView`` objects are typically created by + ``TaggedCorpusReader`` (not directly by nltk users). + """ + + def __init__( + self, + corpus_file, + encoding, + tagged, + group_by_sent, + group_by_para, + sep, + word_tokenizer, + sent_tokenizer, + para_block_reader, + tag_mapping_function=None, + ): + self._tagged = tagged + self._group_by_sent = group_by_sent + self._group_by_para = group_by_para + self._sep = sep + self._word_tokenizer = word_tokenizer + self._sent_tokenizer = sent_tokenizer + self._para_block_reader = para_block_reader + self._tag_mapping_function = tag_mapping_function + StreamBackedCorpusView.__init__(self, corpus_file, encoding=encoding) + + def read_block(self, stream): + """Reads one paragraph at a time.""" + block = [] + for para_str in self._para_block_reader(stream): + para = [] + for sent_str in self._sent_tokenizer.tokenize(para_str): + sent = [ + str2tuple(s, self._sep) + for s in self._word_tokenizer.tokenize(sent_str) + ] + if self._tag_mapping_function: + sent = [(w, self._tag_mapping_function(t)) for (w, t) in sent] + if not self._tagged: + sent = [w for (w, t) in sent] + if self._group_by_sent: + para.append(sent) + else: + para.extend(sent) + if self._group_by_para: + block.append(para) + else: + block.extend(para) + return block + + +# needs to implement simplified tags +class MacMorphoCorpusReader(TaggedCorpusReader): + """ + A corpus reader for the MAC_MORPHO corpus. Each line contains a + single tagged word, using '_' as a separator. Sentence boundaries + are based on the end-sentence tag ('_.'). Paragraph information + is not included in the corpus, so each paragraph returned by + ``self.paras()`` and ``self.tagged_paras()`` contains a single + sentence. + """ + + def __init__(self, root, fileids, encoding="utf8", tagset=None): + TaggedCorpusReader.__init__( + self, + root, + fileids, + sep="_", + word_tokenizer=LineTokenizer(), + sent_tokenizer=RegexpTokenizer(".*\n"), + para_block_reader=self._read_block, + encoding=encoding, + tagset=tagset, + ) + + def _read_block(self, stream): + return read_regexp_block(stream, r".*", r".*_\.") + + +class TimitTaggedCorpusReader(TaggedCorpusReader): + """ + A corpus reader for tagged sentences that are included in the TIMIT corpus. + """ + + def __init__(self, *args, **kwargs): + TaggedCorpusReader.__init__( + self, para_block_reader=read_timit_block, *args, **kwargs + ) + + def paras(self): + raise NotImplementedError("use sents() instead") + + def tagged_paras(self): + raise NotImplementedError("use tagged_sents() instead") diff --git a/nltk/corpus/reader/timit.py b/nltk/corpus/reader/timit.py new file mode 100644 index 0000000..cf258ba --- /dev/null +++ b/nltk/corpus/reader/timit.py @@ -0,0 +1,510 @@ +# Natural Language Toolkit: TIMIT Corpus Reader +# +# Copyright (C) 2001-2007 NLTK Project +# Author: Haejoong Lee +# Steven Bird +# Jacob Perkins +# URL: +# For license information, see LICENSE.TXT + +# [xx] this docstring is out-of-date: +""" +Read tokens, phonemes and audio data from the NLTK TIMIT Corpus. + +This corpus contains selected portion of the TIMIT corpus. + + - 16 speakers from 8 dialect regions + - 1 male and 1 female from each dialect region + - total 130 sentences (10 sentences per speaker. Note that some + sentences are shared among other speakers, especially sa1 and sa2 + are spoken by all speakers.) + - total 160 recording of sentences (10 recordings per speaker) + - audio format: NIST Sphere, single channel, 16kHz sampling, + 16 bit sample, PCM encoding + + +Module contents +=============== + +The timit corpus reader provides 4 functions and 4 data items. + + - utterances + + List of utterances in the corpus. There are total 160 utterances, + each of which corresponds to a unique utterance of a speaker. + Here's an example of an utterance identifier in the list:: + + dr1-fvmh0/sx206 + - _---- _--- + | | | | | + | | | | | + | | | | `--- sentence number + | | | `----- sentence type (a:all, i:shared, x:exclusive) + | | `--------- speaker ID + | `------------ sex (m:male, f:female) + `-------------- dialect region (1..8) + + - speakers + + List of speaker IDs. An example of speaker ID:: + + dr1-fvmh0 + + Note that if you split an item ID with colon and take the first element of + the result, you will get a speaker ID. + + >>> itemid = 'dr1-fvmh0/sx206' + >>> spkrid , sentid = itemid.split('/') + >>> spkrid + 'dr1-fvmh0' + + The second element of the result is a sentence ID. + + - dictionary() + + Phonetic dictionary of words contained in this corpus. This is a Python + dictionary from words to phoneme lists. + + - spkrinfo() + + Speaker information table. It's a Python dictionary from speaker IDs to + records of 10 fields. Speaker IDs the same as the ones in timie.speakers. + Each record is a dictionary from field names to values, and the fields are + as follows:: + + id speaker ID as defined in the original TIMIT speaker info table + sex speaker gender (M:male, F:female) + dr speaker dialect region (1:new england, 2:northern, + 3:north midland, 4:south midland, 5:southern, 6:new york city, + 7:western, 8:army brat (moved around)) + use corpus type (TRN:training, TST:test) + in this sample corpus only TRN is available + recdate recording date + birthdate speaker birth date + ht speaker height + race speaker race (WHT:white, BLK:black, AMR:american indian, + SPN:spanish-american, ORN:oriental,???:unknown) + edu speaker education level (HS:high school, AS:associate degree, + BS:bachelor's degree (BS or BA), MS:master's degree (MS or MA), + PHD:doctorate degree (PhD,JD,MD), ??:unknown) + comments comments by the recorder + +The 4 functions are as follows. + + - tokenized(sentences=items, offset=False) + + Given a list of items, returns an iterator of a list of word lists, + each of which corresponds to an item (sentence). If offset is set to True, + each element of the word list is a tuple of word(string), start offset and + end offset, where offset is represented as a number of 16kHz samples. + + - phonetic(sentences=items, offset=False) + + Given a list of items, returns an iterator of a list of phoneme lists, + each of which corresponds to an item (sentence). If offset is set to True, + each element of the phoneme list is a tuple of word(string), start offset + and end offset, where offset is represented as a number of 16kHz samples. + + - audiodata(item, start=0, end=None) + + Given an item, returns a chunk of audio samples formatted into a string. + When the function is called, if start and end are omitted, the entire + samples of the recording will be returned. If only end is omitted, + samples from the start offset to the end of the recording will be returned. + + - play(data) + + Play the given audio samples. The audio samples can be obtained from the + timit.audiodata function. + +""" +import sys +import time + +from nltk.corpus.reader.api import * +from nltk.internals import import_from_stdlib +from nltk.tree import Tree + + +class TimitCorpusReader(CorpusReader): + """ + Reader for the TIMIT corpus (or any other corpus with the same + file layout and use of file formats). The corpus root directory + should contain the following files: + + - timitdic.txt: dictionary of standard transcriptions + - spkrinfo.txt: table of speaker information + + In addition, the root directory should contain one subdirectory + for each speaker, containing three files for each utterance: + + - .txt: text content of utterances + - .wrd: tokenized text content of utterances + - .phn: phonetic transcription of utterances + - .wav: utterance sound file + """ + + _FILE_RE = r"(\w+-\w+/\w+\.(phn|txt|wav|wrd))|" + r"timitdic\.txt|spkrinfo\.txt" + """A regexp matching fileids that are used by this corpus reader.""" + _UTTERANCE_RE = r"\w+-\w+/\w+\.txt" + + def __init__(self, root, encoding="utf8"): + """ + Construct a new TIMIT corpus reader in the given directory. + :param root: The root directory for this corpus. + """ + # Ensure that wave files don't get treated as unicode data: + if isinstance(encoding, str): + encoding = [(r".*\.wav", None), (".*", encoding)] + + CorpusReader.__init__( + self, root, find_corpus_fileids(root, self._FILE_RE), encoding=encoding + ) + + self._utterances = [ + name[:-4] for name in find_corpus_fileids(root, self._UTTERANCE_RE) + ] + """A list of the utterance identifiers for all utterances in + this corpus.""" + + self._speakerinfo = None + self._root = root + self.speakers = sorted({u.split("/")[0] for u in self._utterances}) + + def fileids(self, filetype=None): + """ + Return a list of file identifiers for the files that make up + this corpus. + + :param filetype: If specified, then ``filetype`` indicates that + only the files that have the given type should be + returned. Accepted values are: ``txt``, ``wrd``, ``phn``, + ``wav``, or ``metadata``, + """ + if filetype is None: + return CorpusReader.fileids(self) + elif filetype in ("txt", "wrd", "phn", "wav"): + return [f"{u}.{filetype}" for u in self._utterances] + elif filetype == "metadata": + return ["timitdic.txt", "spkrinfo.txt"] + else: + raise ValueError("Bad value for filetype: %r" % filetype) + + def utteranceids( + self, dialect=None, sex=None, spkrid=None, sent_type=None, sentid=None + ): + """ + :return: A list of the utterance identifiers for all + utterances in this corpus, or for the given speaker, dialect + region, gender, sentence type, or sentence number, if + specified. + """ + if isinstance(dialect, str): + dialect = [dialect] + if isinstance(sex, str): + sex = [sex] + if isinstance(spkrid, str): + spkrid = [spkrid] + if isinstance(sent_type, str): + sent_type = [sent_type] + if isinstance(sentid, str): + sentid = [sentid] + + utterances = self._utterances[:] + if dialect is not None: + utterances = [u for u in utterances if u[2] in dialect] + if sex is not None: + utterances = [u for u in utterances if u[4] in sex] + if spkrid is not None: + utterances = [u for u in utterances if u[:9] in spkrid] + if sent_type is not None: + utterances = [u for u in utterances if u[11] in sent_type] + if sentid is not None: + utterances = [u for u in utterances if u[10:] in spkrid] + return utterances + + def transcription_dict(self): + """ + :return: A dictionary giving the 'standard' transcription for + each word. + """ + _transcriptions = {} + with self.open("timitdic.txt") as fp: + for line in fp: + if not line.strip() or line[0] == ";": + continue + m = re.match(r"\s*(\S+)\s+/(.*)/\s*$", line) + if not m: + raise ValueError("Bad line: %r" % line) + _transcriptions[m.group(1)] = m.group(2).split() + return _transcriptions + + def spkrid(self, utterance): + return utterance.split("/")[0] + + def sentid(self, utterance): + return utterance.split("/")[1] + + def utterance(self, spkrid, sentid): + return f"{spkrid}/{sentid}" + + def spkrutteranceids(self, speaker): + """ + :return: A list of all utterances associated with a given + speaker. + """ + return [ + utterance + for utterance in self._utterances + if utterance.startswith(speaker + "/") + ] + + def spkrinfo(self, speaker): + """ + :return: A dictionary mapping .. something. + """ + if speaker in self._utterances: + speaker = self.spkrid(speaker) + + if self._speakerinfo is None: + self._speakerinfo = {} + with self.open("spkrinfo.txt") as fp: + for line in fp: + if not line.strip() or line[0] == ";": + continue + rec = line.strip().split(None, 9) + key = f"dr{rec[2]}-{rec[1].lower()}{rec[0].lower()}" + self._speakerinfo[key] = SpeakerInfo(*rec) + + return self._speakerinfo[speaker] + + def phones(self, utterances=None): + results = [] + for fileid in self._utterance_fileids(utterances, ".phn"): + with self.open(fileid) as fp: + for line in fp: + if line.strip(): + results.append(line.split()[-1]) + return results + + def phone_times(self, utterances=None): + """ + offset is represented as a number of 16kHz samples! + """ + results = [] + for fileid in self._utterance_fileids(utterances, ".phn"): + with self.open(fileid) as fp: + for line in fp: + if line.strip(): + results.append( + ( + line.split()[2], + int(line.split()[0]), + int(line.split()[1]), + ) + ) + return results + + def words(self, utterances=None): + results = [] + for fileid in self._utterance_fileids(utterances, ".wrd"): + with self.open(fileid) as fp: + for line in fp: + if line.strip(): + results.append(line.split()[-1]) + return results + + def word_times(self, utterances=None): + results = [] + for fileid in self._utterance_fileids(utterances, ".wrd"): + with self.open(fileid) as fp: + for line in fp: + if line.strip(): + results.append( + ( + line.split()[2], + int(line.split()[0]), + int(line.split()[1]), + ) + ) + return results + + def sents(self, utterances=None): + results = [] + for fileid in self._utterance_fileids(utterances, ".wrd"): + with self.open(fileid) as fp: + results.append([line.split()[-1] for line in fp if line.strip()]) + return results + + def sent_times(self, utterances=None): + # TODO: Check this + return [ + ( + line.split(None, 2)[-1].strip(), + int(line.split()[0]), + int(line.split()[1]), + ) + for fileid in self._utterance_fileids(utterances, ".txt") + for line in self.open(fileid) + if line.strip() + ] + + def phone_trees(self, utterances=None): + if utterances is None: + utterances = self._utterances + if isinstance(utterances, str): + utterances = [utterances] + + trees = [] + for utterance in utterances: + word_times = self.word_times(utterance) + phone_times = self.phone_times(utterance) + sent_times = self.sent_times(utterance) + + while sent_times: + (sent, sent_start, sent_end) = sent_times.pop(0) + trees.append(Tree("S", [])) + while ( + word_times and phone_times and phone_times[0][2] <= word_times[0][1] + ): + trees[-1].append(phone_times.pop(0)[0]) + while word_times and word_times[0][2] <= sent_end: + (word, word_start, word_end) = word_times.pop(0) + trees[-1].append(Tree(word, [])) + while phone_times and phone_times[0][2] <= word_end: + trees[-1][-1].append(phone_times.pop(0)[0]) + while phone_times and phone_times[0][2] <= sent_end: + trees[-1].append(phone_times.pop(0)[0]) + return trees + + # [xx] NOTE: This is currently broken -- we're assuming that the + # fileids are WAV fileids (aka RIFF), but they're actually NIST SPHERE + # fileids. + def wav(self, utterance, start=0, end=None): + # nltk.chunk conflicts with the stdlib module 'chunk' + wave = import_from_stdlib("wave") + + w = wave.open(self.open(utterance + ".wav"), "rb") + + if end is None: + end = w.getnframes() + + # Skip past frames before start, then read the frames we want + w.readframes(start) + frames = w.readframes(end - start) + + # Open a new temporary file -- the wave module requires + # an actual file, and won't work w/ stringio. :( + tf = tempfile.TemporaryFile() + out = wave.open(tf, "w") + + # Write the parameters & data to the new file. + out.setparams(w.getparams()) + out.writeframes(frames) + out.close() + + # Read the data back from the file, and return it. The + # file will automatically be deleted when we return. + tf.seek(0) + return tf.read() + + def audiodata(self, utterance, start=0, end=None): + assert end is None or end > start + headersize = 44 + with self.open(utterance + ".wav") as fp: + if end is None: + data = fp.read() + else: + data = fp.read(headersize + end * 2) + return data[headersize + start * 2 :] + + def _utterance_fileids(self, utterances, extension): + if utterances is None: + utterances = self._utterances + if isinstance(utterances, str): + utterances = [utterances] + return [f"{u}{extension}" for u in utterances] + + def play(self, utterance, start=0, end=None): + """ + Play the given audio sample. + + :param utterance: The utterance id of the sample to play + """ + # Method 1: os audio dev. + try: + import ossaudiodev + + try: + dsp = ossaudiodev.open("w") + dsp.setfmt(ossaudiodev.AFMT_S16_LE) + dsp.channels(1) + dsp.speed(16000) + dsp.write(self.audiodata(utterance, start, end)) + dsp.close() + except OSError as e: + print( + ( + "can't acquire the audio device; please " + "activate your audio device." + ), + file=sys.stderr, + ) + print("system error message:", str(e), file=sys.stderr) + return + except ImportError: + pass + + # Method 2: pygame + try: + # FIXME: this won't work under python 3 + import pygame.mixer + import StringIO + + pygame.mixer.init(16000) + f = StringIO.StringIO(self.wav(utterance, start, end)) + pygame.mixer.Sound(f).play() + while pygame.mixer.get_busy(): + time.sleep(0.01) + return + except ImportError: + pass + + # Method 3: complain. :) + print( + ("you must install pygame or ossaudiodev " "for audio playback."), + file=sys.stderr, + ) + + +class SpeakerInfo: + def __init__( + self, id, sex, dr, use, recdate, birthdate, ht, race, edu, comments=None + ): + self.id = id + self.sex = sex + self.dr = dr + self.use = use + self.recdate = recdate + self.birthdate = birthdate + self.ht = ht + self.race = race + self.edu = edu + self.comments = comments + + def __repr__(self): + attribs = "id sex dr use recdate birthdate ht race edu comments" + args = [f"{attr}={getattr(self, attr)!r}" for attr in attribs.split()] + return "SpeakerInfo(%s)" % (", ".join(args)) + + +def read_timit_block(stream): + """ + Block reader for timit tagged sentences, which are preceded by a sentence + number that will be ignored. + """ + line = stream.readline() + if not line: + return [] + n, sent = line.split(" ", 1) + return [sent] diff --git a/nltk/corpus/reader/toolbox.py b/nltk/corpus/reader/toolbox.py new file mode 100644 index 0000000..5fbf1ad --- /dev/null +++ b/nltk/corpus/reader/toolbox.py @@ -0,0 +1,76 @@ +# Natural Language Toolkit: Toolbox Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Greg Aumann +# Stuart Robinson +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Module for reading, writing and manipulating +Toolbox databases and settings fileids. +""" + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.toolbox import ToolboxData + + +class ToolboxCorpusReader(CorpusReader): + def xml(self, fileids, key=None): + return concat( + [ + ToolboxData(path, enc).parse(key=key) + for (path, enc) in self.abspaths(fileids, True) + ] + ) + + def fields( + self, + fileids, + strip=True, + unwrap=True, + encoding="utf8", + errors="strict", + unicode_fields=None, + ): + return concat( + [ + list( + ToolboxData(fileid, enc).fields( + strip, unwrap, encoding, errors, unicode_fields + ) + ) + for (fileid, enc) in self.abspaths(fileids, include_encoding=True) + ] + ) + + # should probably be done lazily: + def entries(self, fileids, **kwargs): + if "key" in kwargs: + key = kwargs["key"] + del kwargs["key"] + else: + key = "lx" # the default key in MDF + entries = [] + for marker, contents in self.fields(fileids, **kwargs): + if marker == key: + entries.append((contents, [])) + else: + try: + entries[-1][-1].append((marker, contents)) + except IndexError: + pass + return entries + + def words(self, fileids, key="lx"): + return [contents for marker, contents in self.fields(fileids) if marker == key] + + +def demo(): + pass + + +if __name__ == "__main__": + demo() diff --git a/nltk/corpus/reader/twitter.py b/nltk/corpus/reader/twitter.py new file mode 100644 index 0000000..476b9f5 --- /dev/null +++ b/nltk/corpus/reader/twitter.py @@ -0,0 +1,136 @@ +# Natural Language Toolkit: Twitter Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +""" +A reader for corpora that consist of Tweets. It is assumed that the Tweets +have been serialised into line-delimited JSON. +""" + +import json +import os + +from nltk.corpus.reader.api import CorpusReader +from nltk.corpus.reader.util import StreamBackedCorpusView, ZipFilePathPointer, concat +from nltk.tokenize import TweetTokenizer + + +class TwitterCorpusReader(CorpusReader): + r""" + Reader for corpora that consist of Tweets represented as a list of line-delimited JSON. + + Individual Tweets can be tokenized using the default tokenizer, or by a + custom tokenizer specified as a parameter to the constructor. + + Construct a new Tweet corpus reader for a set of documents + located at the given root directory. + + If you made your own tweet collection in a directory called + `twitter-files`, then you can initialise the reader as:: + + from nltk.corpus import TwitterCorpusReader + reader = TwitterCorpusReader(root='/path/to/twitter-files', '.*\.json') + + However, the recommended approach is to set the relevant directory as the + value of the environmental variable `TWITTER`, and then invoke the reader + as follows:: + + root = os.environ['TWITTER'] + reader = TwitterCorpusReader(root, '.*\.json') + + If you want to work directly with the raw Tweets, the `json` library can + be used:: + + import json + for tweet in reader.docs(): + print(json.dumps(tweet, indent=1, sort_keys=True)) + + """ + + CorpusView = StreamBackedCorpusView + """ + The corpus view class used by this reader. + """ + + def __init__( + self, root, fileids=None, word_tokenizer=TweetTokenizer(), encoding="utf8" + ): + """ + :param root: The root directory for this corpus. + :param fileids: A list or regexp specifying the fileids in this corpus. + :param word_tokenizer: Tokenizer for breaking the text of Tweets into + smaller units, including but not limited to words. + """ + CorpusReader.__init__(self, root, fileids, encoding) + + for path in self.abspaths(self._fileids): + if isinstance(path, ZipFilePathPointer): + pass + elif os.path.getsize(path) == 0: + raise ValueError(f"File {path} is empty") + """Check that all user-created corpus files are non-empty.""" + + self._word_tokenizer = word_tokenizer + + def docs(self, fileids=None): + """ + Returns the full Tweet objects, as specified by `Twitter + documentation on Tweets + `_ + + :return: the given file(s) as a list of dictionaries deserialised + from JSON. + :rtype: list(dict) + """ + return concat( + [ + self.CorpusView(path, self._read_tweets, encoding=enc) + for (path, enc, fileid) in self.abspaths(fileids, True, True) + ] + ) + + def strings(self, fileids=None): + """ + Returns only the text content of Tweets in the file(s) + + :return: the given file(s) as a list of Tweets. + :rtype: list(str) + """ + fulltweets = self.docs(fileids) + tweets = [] + for jsono in fulltweets: + try: + text = jsono["text"] + if isinstance(text, bytes): + text = text.decode(self.encoding) + tweets.append(text) + except KeyError: + pass + return tweets + + def tokenized(self, fileids=None): + """ + :return: the given file(s) as a list of the text content of Tweets as + as a list of words, screenanames, hashtags, URLs and punctuation symbols. + + :rtype: list(list(str)) + """ + tweets = self.strings(fileids) + tokenizer = self._word_tokenizer + return [tokenizer.tokenize(t) for t in tweets] + + def _read_tweets(self, stream): + """ + Assumes that each line in ``stream`` is a JSON-serialised object. + """ + tweets = [] + for i in range(10): + line = stream.readline() + if not line: + return tweets + tweet = json.loads(line) + tweets.append(tweet) + return tweets diff --git a/nltk/corpus/reader/udhr.py b/nltk/corpus/reader/udhr.py new file mode 100644 index 0000000..b50aa4c --- /dev/null +++ b/nltk/corpus/reader/udhr.py @@ -0,0 +1,74 @@ +""" +UDHR corpus reader. It mostly deals with encodings. +""" + +from nltk.corpus.reader.plaintext import PlaintextCorpusReader +from nltk.corpus.reader.util import find_corpus_fileids + + +class UdhrCorpusReader(PlaintextCorpusReader): + ENCODINGS = [ + (".*-Latin1$", "latin-1"), + (".*-Hebrew$", "hebrew"), + (".*-Arabic$", "cp1256"), + ("Czech_Cesky-UTF8", "cp1250"), # yeah + ("Polish-Latin2", "cp1250"), + ("Polish_Polski-Latin2", "cp1250"), + (".*-Cyrillic$", "cyrillic"), + (".*-SJIS$", "SJIS"), + (".*-GB2312$", "GB2312"), + (".*-Latin2$", "ISO-8859-2"), + (".*-Greek$", "greek"), + (".*-UTF8$", "utf-8"), + ("Hungarian_Magyar-Unicode", "utf-16-le"), + ("Amahuaca", "latin1"), + ("Turkish_Turkce-Turkish", "latin5"), + ("Lithuanian_Lietuviskai-Baltic", "latin4"), + ("Japanese_Nihongo-EUC", "EUC-JP"), + ("Japanese_Nihongo-JIS", "iso2022_jp"), + ("Chinese_Mandarin-HZ", "hz"), + (r"Abkhaz\-Cyrillic\+Abkh", "cp1251"), + ] + + SKIP = { + # The following files are not fully decodable because they + # were truncated at wrong bytes: + "Burmese_Myanmar-UTF8", + "Japanese_Nihongo-JIS", + "Chinese_Mandarin-HZ", + "Chinese_Mandarin-UTF8", + "Gujarati-UTF8", + "Hungarian_Magyar-Unicode", + "Lao-UTF8", + "Magahi-UTF8", + "Marathi-UTF8", + "Tamil-UTF8", + # Unfortunately, encodings required for reading + # the following files are not supported by Python: + "Vietnamese-VPS", + "Vietnamese-VIQR", + "Vietnamese-TCVN", + "Magahi-Agra", + "Bhojpuri-Agra", + "Esperanto-T61", # latin3 raises an exception + # The following files are encoded for specific fonts: + "Burmese_Myanmar-WinResearcher", + "Armenian-DallakHelv", + "Tigrinya_Tigrigna-VG2Main", + "Amharic-Afenegus6..60375", # ? + "Navaho_Dine-Navajo-Navaho-font", + # What are these? + "Azeri_Azerbaijani_Cyrillic-Az.Times.Cyr.Normal0117", + "Azeri_Azerbaijani_Latin-Az.Times.Lat0117", + # The following files are unintended: + "Czech-Latin2-err", + "Russian_Russky-UTF8~", + } + + def __init__(self, root="udhr"): + fileids = find_corpus_fileids(root, r"(?!README|\.).*") + super().__init__( + root, + [fileid for fileid in fileids if fileid not in self.SKIP], + encoding=self.ENCODINGS, + ) diff --git a/nltk/corpus/reader/util.py b/nltk/corpus/reader/util.py new file mode 100644 index 0000000..858f6fe --- /dev/null +++ b/nltk/corpus/reader/util.py @@ -0,0 +1,812 @@ +# Natural Language Toolkit: Corpus Reader Utilities +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT + +import bisect +import os +import pickle +import re +import tempfile +from functools import reduce +from xml.etree import ElementTree + +from nltk.data import ( + FileSystemPathPointer, + PathPointer, + SeekableUnicodeStreamReader, + ZipFilePathPointer, +) +from nltk.internals import slice_bounds +from nltk.pathsec import open as _secure_open +from nltk.tokenize import wordpunct_tokenize +from nltk.util import AbstractLazySequence, LazyConcatenation, LazySubsequence + +###################################################################### +# { Corpus View +###################################################################### + + +class StreamBackedCorpusView(AbstractLazySequence): + """ + A 'view' of a corpus file, which acts like a sequence of tokens: + it can be accessed by index, iterated over, etc. However, the + tokens are only constructed as-needed -- the entire corpus is + never stored in memory at once. + + The constructor to ``StreamBackedCorpusView`` takes two arguments: + a corpus fileid (specified as a string or as a ``PathPointer``); + and a block reader. A "block reader" is a function that reads + zero or more tokens from a stream, and returns them as a list. A + very simple example of a block reader is: + + >>> def simple_block_reader(stream): + ... return stream.readline().split() + + This simple block reader reads a single line at a time, and + returns a single token (consisting of a string) for each + whitespace-separated substring on the line. + + When deciding how to define the block reader for a given + corpus, careful consideration should be given to the size of + blocks handled by the block reader. Smaller block sizes will + increase the memory requirements of the corpus view's internal + data structures (by 2 integers per block). On the other hand, + larger block sizes may decrease performance for random access to + the corpus. (But note that larger block sizes will *not* + decrease performance for iteration.) + + Internally, ``CorpusView`` maintains a partial mapping from token + index to file position, with one entry per block. When a token + with a given index *i* is requested, the ``CorpusView`` constructs + it as follows: + + 1. First, it searches the toknum/filepos mapping for the token + index closest to (but less than or equal to) *i*. + + 2. Then, starting at the file position corresponding to that + index, it reads one block at a time using the block reader + until it reaches the requested token. + + The toknum/filepos mapping is created lazily: it is initially + empty, but every time a new block is read, the block's + initial token is added to the mapping. (Thus, the toknum/filepos + map has one entry per block.) + + In order to increase efficiency for random access patterns that + have high degrees of locality, the corpus view may cache one or + more blocks. + + :note: Each ``CorpusView`` object internally maintains an open file + object for its underlying corpus file. This file should be + automatically closed when the ``CorpusView`` is garbage collected, + but if you wish to close it manually, use the ``close()`` + method. If you access a ``CorpusView``'s items after it has been + closed, the file object will be automatically re-opened. + + :warning: If the contents of the file are modified during the + lifetime of the ``CorpusView``, then the ``CorpusView``'s behavior + is undefined. + + :warning: If a unicode encoding is specified when constructing a + ``CorpusView``, then the block reader may only call + ``stream.seek()`` with offsets that have been returned by + ``stream.tell()``; in particular, calling ``stream.seek()`` with + relative offsets, or with offsets based on string lengths, may + lead to incorrect behavior. + + :ivar _block_reader: The function used to read + a single block from the underlying file stream. + :ivar _toknum: A list containing the token index of each block + that has been processed. In particular, ``_toknum[i]`` is the + token index of the first token in block ``i``. Together + with ``_filepos``, this forms a partial mapping between token + indices and file positions. + :ivar _filepos: A list containing the file position of each block + that has been processed. In particular, ``_toknum[i]`` is the + file position of the first character in block ``i``. Together + with ``_toknum``, this forms a partial mapping between token + indices and file positions. + :ivar _stream: The stream used to access the underlying corpus file. + :ivar _len: The total number of tokens in the corpus, if known; + or None, if the number of tokens is not yet known. + :ivar _eofpos: The character position of the last character in the + file. This is calculated when the corpus view is initialized, + and is used to decide when the end of file has been reached. + :ivar _cache: A cache of the most recently read block. It + is encoded as a tuple (start_toknum, end_toknum, tokens), where + start_toknum is the token index of the first token in the block; + end_toknum is the token index of the first token not in the + block; and tokens is a list of the tokens in the block. + """ + + def __init__(self, fileid, block_reader=None, startpos=0, encoding="utf8"): + """ + Create a new corpus view, based on the file ``fileid``, and + read with ``block_reader``. See the class documentation + for more information. + + :param fileid: The path to the file that is read by this + corpus view. ``fileid`` can either be a string or a + ``PathPointer``. + + :param startpos: The file position at which the view will + start reading. This can be used to skip over preface + sections. + + :param encoding: The unicode encoding that should be used to + read the file's contents. If no encoding is specified, + then the file's contents will be read as a non-unicode + string (i.e., a str). + """ + if block_reader: + self.read_block = block_reader + # Initialize our toknum/filepos mapping. + self._toknum = [0] + self._filepos = [startpos] + self._encoding = encoding + # We don't know our length (number of tokens) yet. + self._len = None + + self._fileid = fileid + self._stream = None + + self._current_toknum = None + """This variable is set to the index of the next token that + will be read, immediately before ``self.read_block()`` is + called. This is provided for the benefit of the block + reader, which under rare circumstances may need to know + the current token number.""" + + self._current_blocknum = None + """This variable is set to the index of the next block that + will be read, immediately before ``self.read_block()`` is + called. This is provided for the benefit of the block + reader, which under rare circumstances may need to know + the current block number.""" + + # Find the length of the file. + try: + if isinstance(self._fileid, PathPointer): + self._eofpos = self._fileid.file_size() + else: + self._eofpos = os.stat(self._fileid).st_size + except Exception as exc: + raise ValueError(f"Unable to open or access {fileid!r} -- {exc}") from exc + + # Maintain a cache of the most recently read block, to + # increase efficiency of random access. + self._cache = (-1, -1, None) + + fileid = property( + lambda self: self._fileid, + doc=""" + The fileid of the file that is accessed by this view. + + :type: str or PathPointer""", + ) + + def read_block(self, stream): + """ + Read a block from the input stream. + + :return: a block of tokens from the input stream + :rtype: list(any) + :param stream: an input stream + :type stream: stream + """ + raise NotImplementedError("Abstract Method") + + def _open(self): + """ + Open the file stream associated with this corpus view. This + will be called performed if any value is read from the view + while its file stream is closed. + """ + if isinstance(self._fileid, PathPointer): + self._stream = self._fileid.open(self._encoding) + elif self._encoding: + self._stream = SeekableUnicodeStreamReader( + _secure_open(self._fileid, "rb"), self._encoding + ) + else: + self._stream = _secure_open(self._fileid, "rb") + + def close(self): + """ + Close the file stream associated with this corpus view. This + can be useful if you are worried about running out of file + handles (although the stream should automatically be closed + upon garbage collection of the corpus view). If the corpus + view is accessed after it is closed, it will be automatically + re-opened. + """ + if self._stream is not None: + self._stream.close() + self._stream = None + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def __len__(self): + if self._len is None: + # iterate_from() sets self._len when it reaches the end + # of the file: + for tok in self.iterate_from(self._toknum[-1]): + pass + return self._len + + def __getitem__(self, i): + if isinstance(i, slice): + start, stop = slice_bounds(self, i) + # Check if it's in the cache. + offset = self._cache[0] + if offset <= start and stop <= self._cache[1]: + return self._cache[2][start - offset : stop - offset] + # Construct & return the result. + return LazySubsequence(self, start, stop) + else: + # Handle negative indices + if i < 0: + i += len(self) + if i < 0: + raise IndexError("index out of range") + # Check if it's in the cache. + offset = self._cache[0] + if offset <= i < self._cache[1]: + return self._cache[2][i - offset] + # Use iterate_from to extract it. + try: + return next(self.iterate_from(i)) + except StopIteration as e: + raise IndexError("index out of range") from e + + # If we wanted to be thread-safe, then this method would need to + # do some locking. + def iterate_from(self, start_tok): + # Start by feeding from the cache, if possible. + if self._cache[0] <= start_tok < self._cache[1]: + for tok in self._cache[2][start_tok - self._cache[0] :]: + yield tok + start_tok += 1 + + # Decide where in the file we should start. If `start` is in + # our mapping, then we can jump straight to the correct block; + # otherwise, start at the last block we've processed. + if start_tok < self._toknum[-1]: + block_index = bisect.bisect_right(self._toknum, start_tok) - 1 + toknum = self._toknum[block_index] + filepos = self._filepos[block_index] + else: + block_index = len(self._toknum) - 1 + toknum = self._toknum[-1] + filepos = self._filepos[-1] + + # Open the stream, if it's not open already. + if self._stream is None: + self._open() + + # If the file is empty, the while loop will never run. + # This *seems* to be all the state we need to set: + if self._eofpos == 0: + self._len = 0 + + # Each iteration through this loop, we read a single block + # from the stream. + while filepos < self._eofpos: + # Read the next block. + self._stream.seek(filepos) + self._current_toknum = toknum + self._current_blocknum = block_index + tokens = self.read_block(self._stream) + assert isinstance(tokens, (tuple, list, AbstractLazySequence)), ( + "block reader %s() should return list or tuple." + % self.read_block.__name__ + ) + num_toks = len(tokens) + new_filepos = self._stream.tell() + assert ( + new_filepos > filepos + ), "block reader %s() should consume at least 1 byte (filepos=%d)" % ( + self.read_block.__name__, + filepos, + ) + + # Update our cache. + self._cache = (toknum, toknum + num_toks, list(tokens)) + + # Update our mapping. + assert toknum <= self._toknum[-1] + if num_toks > 0: + block_index += 1 + if toknum == self._toknum[-1]: + assert new_filepos > self._filepos[-1] # monotonic! + self._filepos.append(new_filepos) + self._toknum.append(toknum + num_toks) + else: + # Check for consistency: + assert ( + new_filepos == self._filepos[block_index] + ), "inconsistent block reader (num chars read)" + assert ( + toknum + num_toks == self._toknum[block_index] + ), "inconsistent block reader (num tokens returned)" + + # If we reached the end of the file, then update self._len + if new_filepos == self._eofpos: + self._len = toknum + num_toks + # Generate the tokens in this block (but skip any tokens + # before start_tok). Note that between yields, our state + # may be modified. + for tok in tokens[max(0, start_tok - toknum) :]: + yield tok + # If we're at the end of the file, then we're done. + assert new_filepos <= self._eofpos + if new_filepos == self._eofpos: + break + # Update our indices + toknum += num_toks + filepos = new_filepos + + # If we reach this point, then we should know our length. + assert self._len is not None + # Enforce closing of stream once we reached end of file + # We should have reached EOF once we're out of the while loop. + self.close() + + # Use concat for these, so we can use a ConcatenatedCorpusView + # when possible. + def __add__(self, other): + return concat([self, other]) + + def __radd__(self, other): + return concat([other, self]) + + def __mul__(self, count): + return concat([self] * count) + + def __rmul__(self, count): + return concat([self] * count) + + +class ConcatenatedCorpusView(AbstractLazySequence): + """ + A 'view' of a corpus file that joins together one or more + ``StreamBackedCorpusViews``. At most + one file handle is left open at any time. + """ + + def __init__(self, corpus_views): + self._pieces = corpus_views + """A list of the corpus subviews that make up this + concatenation.""" + + self._offsets = [0] + """A list of offsets, indicating the index at which each + subview begins. In particular:: + offsets[i] = sum([len(p) for p in pieces[:i]])""" + + self._open_piece = None + """The most recently accessed corpus subview (or None). + Before a new subview is accessed, this subview will be closed.""" + + def __len__(self): + if len(self._offsets) <= len(self._pieces): + # Iterate to the end of the corpus. + for tok in self.iterate_from(self._offsets[-1]): + pass + + return self._offsets[-1] + + def close(self): + for piece in self._pieces: + piece.close() + + def iterate_from(self, start_tok): + piecenum = bisect.bisect_right(self._offsets, start_tok) - 1 + + while piecenum < len(self._pieces): + offset = self._offsets[piecenum] + piece = self._pieces[piecenum] + + # If we've got another piece open, close it first. + if self._open_piece is not piece: + if self._open_piece is not None: + self._open_piece.close() + self._open_piece = piece + + # Get everything we can from this piece. + yield from piece.iterate_from(max(0, start_tok - offset)) + + # Update the offset table. + if piecenum + 1 == len(self._offsets): + self._offsets.append(self._offsets[-1] + len(piece)) + + # Move on to the next piece. + piecenum += 1 + + +def concat(docs): + """ + Concatenate together the contents of multiple documents from a + single corpus, using an appropriate concatenation function. This + utility function is used by corpus readers when the user requests + more than one document at a time. + """ + if len(docs) == 1: + return docs[0] + if len(docs) == 0: + raise ValueError("concat() expects at least one object!") + + types = {d.__class__ for d in docs} + + # If they're all strings, use string concatenation. + if all(isinstance(doc, str) for doc in docs): + return "".join(docs) + + # If they're all corpus views, then use ConcatenatedCorpusView. + for typ in types: + if not issubclass(typ, (StreamBackedCorpusView, ConcatenatedCorpusView)): + break + else: + return ConcatenatedCorpusView(docs) + + # If they're all lazy sequences, use a lazy concatenation + for typ in types: + if not issubclass(typ, AbstractLazySequence): + break + else: + return LazyConcatenation(docs) + + # Otherwise, see what we can do: + if len(types) == 1: + typ = list(types)[0] + + if issubclass(typ, list): + return reduce((lambda a, b: a + b), docs, []) + + if issubclass(typ, tuple): + return reduce((lambda a, b: a + b), docs, ()) + + if ElementTree.iselement(typ): + xmltree = ElementTree.Element("documents") + for doc in docs: + xmltree.append(doc) + return xmltree + + # No method found! + raise ValueError("Don't know how to concatenate types: %r" % types) + + +###################################################################### +# { Block Readers +###################################################################### + + +def read_whitespace_block(stream): + toks = [] + for i in range(20): # Read 20 lines at a time. + toks.extend(stream.readline().split()) + return toks + + +def read_wordpunct_block(stream): + toks = [] + for i in range(20): # Read 20 lines at a time. + toks.extend(wordpunct_tokenize(stream.readline())) + return toks + + +def read_line_block(stream): + toks = [] + for i in range(20): + line = stream.readline() + if not line: + return toks + toks.append(line.rstrip("\n")) + return toks + + +def read_blankline_block(stream): + s = "" + while True: + line = stream.readline() + # End of file: + if not line: + if s: + return [s] + else: + return [] + # Blank line: + elif line and not line.strip(): + if s: + return [s] + # Other line: + else: + s += line + + +def read_alignedsent_block(stream): + s = "" + while True: + line = stream.readline() + if line[0] == "=" or line[0] == "\n" or line[:2] == "\r\n": + continue + # End of file: + if not line: + if s: + return [s] + else: + return [] + # Other line: + else: + s += line + if re.match(r"^\d+-\d+", line) is not None: + return [s] + + +def read_regexp_block(stream, start_re, end_re=None): + """ + Read a sequence of tokens from a stream, where tokens begin with + lines that match ``start_re``. If ``end_re`` is specified, then + tokens end with lines that match ``end_re``; otherwise, tokens end + whenever the next line matching ``start_re`` or EOF is found. + """ + # Scan until we find a line matching the start regexp. + while True: + line = stream.readline() + if not line: + return [] # end of file. + if re.match(start_re, line): + break + + # Scan until we find another line matching the regexp, or EOF. + lines = [line] + while True: + oldpos = stream.tell() + line = stream.readline() + # End of file: + if not line: + return ["".join(lines)] + # End of token: + if end_re is not None and re.match(end_re, line): + return ["".join(lines)] + # Start of new token: backup to just before it starts, and + # return the token we've already collected. + if end_re is None and re.match(start_re, line): + stream.seek(oldpos) + return ["".join(lines)] + # Anything else is part of the token. + lines.append(line) + + +def read_sexpr_block(stream, block_size=16384, comment_char=None): + """ + Read a sequence of s-expressions from the stream, and leave the + stream's file position at the end the last complete s-expression + read. This function will always return at least one s-expression, + unless there are no more s-expressions in the file. + + If the file ends in in the middle of an s-expression, then that + incomplete s-expression is returned when the end of the file is + reached. + + :param block_size: The default block size for reading. If an + s-expression is longer than one block, then more than one + block will be read. + :param comment_char: A character that marks comments. Any lines + that begin with this character will be stripped out. + (If spaces or tabs precede the comment character, then the + line will not be stripped.) + """ + start = stream.tell() + block = stream.read(block_size) + encoding = getattr(stream, "encoding", None) + assert encoding is not None or isinstance(block, str) + if encoding not in (None, "utf-8"): + import warnings + + warnings.warn( + "Parsing may fail, depending on the properties " + "of the %s encoding!" % encoding + ) + # (e.g., the utf-16 encoding does not work because it insists + # on adding BOMs to the beginning of encoded strings.) + + if comment_char: + COMMENT = re.compile("(?m)^%s.*$" % re.escape(comment_char)) + while True: + try: + # If we're stripping comments, then make sure our block ends + # on a line boundary; and then replace any comments with + # space characters. (We can't just strip them out -- that + # would make our offset wrong.) + if comment_char: + block += stream.readline() + block = re.sub(COMMENT, _sub_space, block) + # Read the block. + tokens, offset = _parse_sexpr_block(block) + # Skip whitespace + offset = re.compile(r"\s*").search(block, offset).end() + + # Move to the end position. + if encoding is None: + stream.seek(start + offset) + else: + stream.seek(start + len(block[:offset].encode(encoding))) + + # Return the list of tokens we processed + return tokens + except ValueError as e: + if e.args[0] == "Block too small": + next_block = stream.read(block_size) + if next_block: + block += next_block + continue + else: + # The file ended mid-sexpr -- return what we got. + return [block.strip()] + else: + raise + + +def _sub_space(m): + """Helper function: given a regexp match, return a string of + spaces that's the same length as the matched string.""" + return " " * (m.end() - m.start()) + + +def _parse_sexpr_block(block): + tokens = [] + start = end = 0 + + while end < len(block): + m = re.compile(r"\S").search(block, end) + if not m: + return tokens, end + + start = m.start() + + # Case 1: sexpr is not parenthesized. + if m.group() != "(": + m2 = re.compile(r"[\s(]").search(block, start) + if m2: + end = m2.start() + else: + if tokens: + return tokens, end + raise ValueError("Block too small") + + # Case 2: parenthesized sexpr. + else: + nesting = 0 + for m in re.compile(r"[()]").finditer(block, start): + if m.group() == "(": + nesting += 1 + else: + nesting -= 1 + if nesting == 0: + end = m.end() + break + else: + if tokens: + return tokens, end + raise ValueError("Block too small") + + tokens.append(block[start:end]) + + return tokens, end + + +###################################################################### +# { Finding Corpus Items +###################################################################### + + +def find_corpus_fileids(root, regexp): + if not isinstance(root, PathPointer): + raise TypeError("find_corpus_fileids: expected a PathPointer") + regexp += "$" + + # Find fileids in a zipfile: scan the zipfile's namelist. Filter + # out entries that end in '/' -- they're directories. + if isinstance(root, ZipFilePathPointer): + fileids = [ + name[len(root.entry) :] + for name in root.zipfile.namelist() + if not name.endswith("/") + ] + items = [name for name in fileids if re.match(regexp, name)] + return sorted(items) + + # Find fileids in a directory: use os.walk to search subdirectories, + # but do not descend into symlinked directories that resolve outside + # the corpus root. + elif isinstance(root, FileSystemPathPointer): + items = [] + resolved_root = os.path.realpath(root.path) + + for dirname, subdirs, fileids in os.walk(root.path): + dirname_real = os.path.realpath(dirname) + try: + if os.path.commonpath([resolved_root, dirname_real]) != resolved_root: + subdirs[:] = [] + continue + except ValueError: + subdirs[:] = [] + continue + + pruned_subdirs = [] + for subdir in subdirs: + full_subdir = os.path.join(dirname, subdir) + subdir_real = os.path.realpath(full_subdir) + try: + inside_root = ( + os.path.commonpath([resolved_root, subdir_real]) + == resolved_root + ) + except ValueError: + inside_root = False + + if inside_root and subdir != ".svn": + pruned_subdirs.append(subdir) + + subdirs[:] = pruned_subdirs + + prefix = "".join("%s/" % p for p in _path_from(root.path, dirname)) + items += [ + prefix + fileid + for fileid in fileids + if re.match(regexp, prefix + fileid) + ] + return sorted(items) + + # HuggingFace PathPointer: delegate to its fileids() method (duck typing, + # avoids a circular import of nltk.huggingface.dataset here). + elif hasattr(root, "fileids"): + return [fid for fid in root.fileids() if re.match(regexp, fid)] + + else: + raise AssertionError("Don't know how to handle %r" % root) + + +def _path_from(parent, child): + if os.path.split(parent)[1] == "": + parent = os.path.split(parent)[0] + path = [] + while parent != child: + child, dirname = os.path.split(child) + path.insert(0, dirname) + assert os.path.split(child)[0] != child + return path + + +###################################################################### +# { Paragraph structure in Treebank files +###################################################################### + + +def tagged_treebank_para_block_reader(stream): + # Read the next paragraph. + para = "" + while True: + line = stream.readline() + # End of paragraph: + if re.match(r"======+\s*$", line): + if para.strip(): + return [para] + # End of file: + elif line == "": + if para.strip(): + return [para] + else: + return [] + # Content line: + else: + para += line diff --git a/nltk/corpus/reader/verbnet.py b/nltk/corpus/reader/verbnet.py new file mode 100644 index 0000000..37e5305 --- /dev/null +++ b/nltk/corpus/reader/verbnet.py @@ -0,0 +1,652 @@ +# Natural Language Toolkit: Verbnet Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +An NLTK interface to the VerbNet verb lexicon + +For details about VerbNet see: +https://verbs.colorado.edu/~mpalmer/projects/verbnet.html +""" + +import re +import textwrap +from collections import defaultdict + +from nltk.corpus.reader.xmldocs import XMLCorpusReader + + +class VerbnetCorpusReader(XMLCorpusReader): + """ + An NLTK interface to the VerbNet verb lexicon. + + From the VerbNet site: "VerbNet (VN) (Kipper-Schuler 2006) is the largest + on-line verb lexicon currently available for English. It is a hierarchical + domain-independent, broad-coverage verb lexicon with mappings to other + lexical resources such as WordNet (Miller, 1990; Fellbaum, 1998), XTAG + (XTAG Research Group, 2001), and FrameNet (Baker et al., 1998)." + + For details about VerbNet see: + https://verbs.colorado.edu/~mpalmer/projects/verbnet.html + """ + + #: Supported VerbNet versions. + SUPPORTED_VERSIONS = ("2.1", "3.2", "3.3") + + # No unicode encoding param, since the data files are all XML. + def __init__(self, root, fileids, wrap_etree=False, version="2.1"): + """ + :param root: The root directory for the corpus. + :param fileids: A list or regexp specifying the fileids in the corpus. + :param wrap_etree: If true, wrap the ElementTree in an ElementWrapper. + :param version: The VerbNet version string (default ``"2.1"``). + NLTK ships VerbNet 2.1 via ``nltk.download('verbnet')``. + Use ``"3.2"`` or ``"3.3"`` when pointing *root* at a local + copy of VerbNet 3.2 or 3.3. + """ + if version not in self.SUPPORTED_VERSIONS: + raise ValueError( + f"VerbNet version {version!r} is not supported. " + f"Supported versions: {self.SUPPORTED_VERSIONS}" + ) + self._version = version + XMLCorpusReader.__init__(self, root, fileids, wrap_etree) + + self._lemma_to_class = defaultdict(list) + """A dictionary mapping from verb lemma strings to lists of + VerbNet class identifiers.""" + + self._wordnet_to_class = defaultdict(list) + """A dictionary mapping from wordnet identifier strings to + lists of VerbNet class identifiers.""" + + self._class_to_fileid = {} + """A dictionary mapping from class identifiers to + corresponding file identifiers. The keys of this dictionary + provide a complete list of all classes and subclasses.""" + + self._shortid_to_longid = {} + + # Initialize the dictionaries. Use the quick (regexp-based) + # method instead of the slow (xml-based) method, because it + # runs 2-30 times faster. + self._quick_index() + + @property + def version(self): + """The VerbNet version string for this corpus instance.""" + return self._version + + _LONGID_RE = re.compile(r"([A-Za-z_]+)-([\d.-]+)$") + """Regular expression that matches (and decomposes) longids""" + + _SHORTID_RE = re.compile(r"[\d.\-]+$") + """Regular expression that matches shortids""" + + _INDEX_RE = re.compile( + r']+>|' r'' + ) + """Regular expression used by ``_index()`` to quickly scan the corpus + for basic information.""" + + def lemmas(self, vnclass=None): + """ + Return a list of all verb lemmas that appear in any class, or + in the ``classid`` if specified. + """ + if vnclass is None: + return sorted(self._lemma_to_class.keys()) + else: + # [xx] should this include subclass members? + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + return [member.get("name") for member in vnclass.findall("MEMBERS/MEMBER")] + + def wordnetids(self, vnclass=None): + """ + Return a list of all wordnet identifiers that appear in any + class, or in ``classid`` if specified. + """ + if vnclass is None: + return sorted(self._wordnet_to_class.keys()) + else: + # [xx] should this include subclass members? + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + return sum( + ( + member.get("wn", "").split() + for member in vnclass.findall("MEMBERS/MEMBER") + ), + [], + ) + + def classids(self, lemma=None, wordnetid=None, fileid=None, classid=None): + """ + Return a list of the VerbNet class identifiers. If a file + identifier is specified, then return only the VerbNet class + identifiers for classes (and subclasses) defined by that file. + If a lemma is specified, then return only VerbNet class + identifiers for classes that contain that lemma as a member. + If a wordnetid is specified, then return only identifiers for + classes that contain that wordnetid as a member. If a classid + is specified, then return only identifiers for subclasses of + the specified VerbNet class. + If nothing is specified, return all classids within VerbNet + """ + if fileid is not None: + return [c for (c, f) in self._class_to_fileid.items() if f == fileid] + elif lemma is not None: + return self._lemma_to_class[lemma] + elif wordnetid is not None: + return self._wordnet_to_class[wordnetid] + elif classid is not None: + xmltree = self.vnclass(classid) + return [ + subclass.get("ID") + for subclass in xmltree.findall("SUBCLASSES/VNSUBCLASS") + ] + else: + return sorted(self._class_to_fileid.keys()) + + def vnclass(self, fileid_or_classid): + """Returns VerbNet class ElementTree + + Return an ElementTree containing the xml for the specified + VerbNet class. + + :param fileid_or_classid: An identifier specifying which class + should be returned. Can be a file identifier (such as + ``'put-9.1.xml'``), or a VerbNet class identifier (such as + ``'put-9.1'``) or a short VerbNet class identifier (such as + ``'9.1'``). + """ + # File identifier: just return the xml. + if fileid_or_classid in self._fileids: + return self.xml(fileid_or_classid) + + # Class identifier: get the xml, and find the right elt. + classid = self.longid(fileid_or_classid) + if classid in self._class_to_fileid: + fileid = self._class_to_fileid[self.longid(classid)] + tree = self.xml(fileid) + if classid == tree.get("ID"): + return tree + else: + for subclass in tree.findall(".//VNSUBCLASS"): + if classid == subclass.get("ID"): + return subclass + else: + assert False # we saw it during _index()! + + else: + raise ValueError(f"Unknown identifier {fileid_or_classid}") + + def fileids(self, vnclass_ids=None): + """ + Return a list of fileids that make up this corpus. If + ``vnclass_ids`` is specified, then return the fileids that make + up the specified VerbNet class(es). + """ + if vnclass_ids is None: + return self._fileids + elif isinstance(vnclass_ids, str): + return [self._class_to_fileid[self.longid(vnclass_ids)]] + else: + return [ + self._class_to_fileid[self.longid(vnclass_id)] + for vnclass_id in vnclass_ids + ] + + def frames(self, vnclass): + """Given a VerbNet class, this method returns VerbNet frames + + The members returned are: + 1) Example + 2) Description + 3) Syntax + 4) Semantics + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + :return: frames - a list of frame dictionaries + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + frames = [] + vnframes = vnclass.findall("FRAMES/FRAME") + for vnframe in vnframes: + frames.append( + { + "example": self._get_example_within_frame(vnframe), + "description": self._get_description_within_frame(vnframe), + "syntax": self._get_syntactic_list_within_frame(vnframe), + "semantics": self._get_semantics_within_frame(vnframe), + } + ) + return frames + + def subclasses(self, vnclass): + """Returns subclass ids, if any exist + + Given a VerbNet class, this method returns subclass ids (if they exist) + in a list of strings. + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + :return: list of subclasses + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + + subclasses = [ + subclass.get("ID") for subclass in vnclass.findall("SUBCLASSES/VNSUBCLASS") + ] + return subclasses + + def themroles(self, vnclass): + """Returns thematic roles participating in a VerbNet class + + Members returned as part of roles are- + 1) Type + 2) Modifiers + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + :return: themroles: A list of thematic roles in the VerbNet class + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + + themroles = [] + for trole in vnclass.findall("THEMROLES/THEMROLE"): + themroles.append( + { + "type": trole.get("type"), + "modifiers": [ + {"value": restr.get("Value"), "type": restr.get("type")} + for restr in trole.findall("SELRESTRS/SELRESTR") + ], + } + ) + return themroles + + ###################################################################### + # { Index Initialization + ###################################################################### + + def _index(self): + """ + Initialize the indexes ``_lemma_to_class``, + ``_wordnet_to_class``, and ``_class_to_fileid`` by scanning + through the corpus fileids. This is fast if ElementTree + uses the C implementation (<0.1 secs), but quite slow (>10 secs) + if only the python implementation is available. + """ + for fileid in self._fileids: + self._index_helper(self.xml(fileid), fileid) + + def _index_helper(self, xmltree, fileid): + """Helper for ``_index()``""" + vnclass = xmltree.get("ID") + self._class_to_fileid[vnclass] = fileid + self._shortid_to_longid[self.shortid(vnclass)] = vnclass + for member in xmltree.findall("MEMBERS/MEMBER"): + self._lemma_to_class[member.get("name")].append(vnclass) + for wn in member.get("wn", "").split(): + self._wordnet_to_class[wn].append(vnclass) + for subclass in xmltree.findall("SUBCLASSES/VNSUBCLASS"): + self._index_helper(subclass, fileid) + + def _quick_index(self): + """ + Initialize the indexes ``_lemma_to_class``, + ``_wordnet_to_class``, and ``_class_to_fileid`` by scanning + through the corpus fileids. This doesn't do proper xml parsing, + but is good enough to find everything in the standard VerbNet + corpus -- and it runs about 30 times faster than xml parsing + (with the python ElementTree; only 2-3 times faster + if ElementTree uses the C implementation). + """ + # nb: if we got rid of wordnet_to_class, this would run 2-3 + # times faster. + for fileid in self._fileids: + vnclass = fileid[:-4] # strip the '.xml' + self._class_to_fileid[vnclass] = fileid + self._shortid_to_longid[self.shortid(vnclass)] = vnclass + with self.open(fileid) as fp: + for m in self._INDEX_RE.finditer(fp.read()): + groups = m.groups() + if groups[0] is not None: + self._lemma_to_class[groups[0]].append(vnclass) + for wn in groups[1].split(): + self._wordnet_to_class[wn].append(vnclass) + elif groups[2] is not None: + self._class_to_fileid[groups[2]] = fileid + vnclass = groups[2] # for elts. + self._shortid_to_longid[self.shortid(vnclass)] = vnclass + else: + assert False, "unexpected match condition" + + ###################################################################### + # { Identifier conversion + ###################################################################### + + def longid(self, shortid): + """Returns longid of a VerbNet class + + Given a short VerbNet class identifier (eg '37.10'), map it + to a long id (eg 'confess-37.10'). If ``shortid`` is already a + long id, then return it as-is""" + if self._LONGID_RE.match(shortid): + return shortid # it's already a longid. + elif not self._SHORTID_RE.match(shortid): + raise ValueError("vnclass identifier %r not found" % shortid) + try: + return self._shortid_to_longid[shortid] + except KeyError as e: + raise ValueError("vnclass identifier %r not found" % shortid) from e + + def shortid(self, longid): + """Returns shortid of a VerbNet class + + Given a long VerbNet class identifier (eg 'confess-37.10'), + map it to a short id (eg '37.10'). If ``longid`` is already a + short id, then return it as-is.""" + if self._SHORTID_RE.match(longid): + return longid # it's already a shortid. + m = self._LONGID_RE.match(longid) + if m: + return m.group(2) + else: + raise ValueError("vnclass identifier %r not found" % longid) + + ###################################################################### + # { Frame access utility functions + ###################################################################### + + def _get_semantics_within_frame(self, vnframe): + """Returns semantics within a single frame + + A utility function to retrieve semantics within a frame in VerbNet + Members of the semantics dictionary: + 1) Predicate value + 2) Arguments + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + :return: semantics: semantics dictionary + """ + semantics_within_single_frame = [] + for pred in vnframe.findall("SEMANTICS/PRED"): + arguments = [ + {"type": arg.get("type"), "value": arg.get("value")} + for arg in pred.findall("ARGS/ARG") + ] + semantics_within_single_frame.append( + { + "predicate_value": pred.get("value"), + "arguments": arguments, + "negated": pred.get("bool") == "!", + } + ) + return semantics_within_single_frame + + def _get_example_within_frame(self, vnframe): + """Returns example within a frame + + A utility function to retrieve an example within a frame in VerbNet. + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + :return: example_text: The example sentence for this particular frame + """ + example_element = vnframe.find("EXAMPLES/EXAMPLE") + if example_element is not None: + example_text = example_element.text + else: + example_text = "" + return example_text + + def _get_description_within_frame(self, vnframe): + """Returns member description within frame + + A utility function to retrieve a description of participating members + within a frame in VerbNet. + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + :return: description: a description dictionary with members - primary and secondary + """ + description_element = vnframe.find("DESCRIPTION") + return { + "primary": description_element.attrib["primary"], + "secondary": description_element.get("secondary", ""), + } + + def _get_syntactic_list_within_frame(self, vnframe): + """Returns semantics within a frame + + A utility function to retrieve semantics within a frame in VerbNet. + Members of the syntactic dictionary: + 1) POS Tag + 2) Modifiers + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + :return: syntax_within_single_frame + """ + syntax_within_single_frame = [] + for elt in vnframe.find("SYNTAX"): + pos_tag = elt.tag + modifiers = dict() + modifiers["value"] = elt.get("value") if "value" in elt.attrib else "" + modifiers["selrestrs"] = [ + {"value": restr.get("Value"), "type": restr.get("type")} + for restr in elt.findall("SELRESTRS/SELRESTR") + ] + modifiers["synrestrs"] = [ + {"value": restr.get("Value"), "type": restr.get("type")} + for restr in elt.findall("SYNRESTRS/SYNRESTR") + ] + syntax_within_single_frame.append( + {"pos_tag": pos_tag, "modifiers": modifiers} + ) + return syntax_within_single_frame + + ###################################################################### + # { Pretty Printing + ###################################################################### + + def pprint(self, vnclass): + """Returns pretty printed version of a VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet class. + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + + s = vnclass.get("ID") + "\n" + s += self.pprint_subclasses(vnclass, indent=" ") + "\n" + s += self.pprint_members(vnclass, indent=" ") + "\n" + s += " Thematic roles:\n" + s += self.pprint_themroles(vnclass, indent=" ") + "\n" + s += " Frames:\n" + s += self.pprint_frames(vnclass, indent=" ") + return s + + def pprint_subclasses(self, vnclass, indent=""): + """Returns pretty printed version of subclasses of VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet class's subclasses. + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + + subclasses = self.subclasses(vnclass) + if not subclasses: + subclasses = ["(none)"] + s = "Subclasses: " + " ".join(subclasses) + return textwrap.fill( + s, 70, initial_indent=indent, subsequent_indent=indent + " " + ) + + def pprint_members(self, vnclass, indent=""): + """Returns pretty printed version of members in a VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet class's member verbs. + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + + members = self.lemmas(vnclass) + if not members: + members = ["(none)"] + s = "Members: " + " ".join(members) + return textwrap.fill( + s, 70, initial_indent=indent, subsequent_indent=indent + " " + ) + + def pprint_themroles(self, vnclass, indent=""): + """Returns pretty printed version of thematic roles in a VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet class's thematic roles. + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + + pieces = [] + for themrole in self.themroles(vnclass): + piece = indent + "* " + themrole.get("type") + modifiers = [ + modifier["value"] + modifier["type"] + for modifier in themrole["modifiers"] + ] + if modifiers: + piece += "[{}]".format(" ".join(modifiers)) + pieces.append(piece) + return "\n".join(pieces) + + def pprint_frames(self, vnclass, indent=""): + """Returns pretty version of all frames in a VerbNet class + + Return a string containing a pretty-printed representation of + the list of frames within the VerbNet class. + + :param vnclass: A VerbNet class identifier; or an ElementTree + containing the xml contents of a VerbNet class. + """ + if isinstance(vnclass, str): + vnclass = self.vnclass(vnclass) + pieces = [] + for vnframe in self.frames(vnclass): + pieces.append(self._pprint_single_frame(vnframe, indent)) + return "\n".join(pieces) + + def _pprint_single_frame(self, vnframe, indent=""): + """Returns pretty printed version of a single frame in a VerbNet class + + Returns a string containing a pretty-printed representation of + the given frame. + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + """ + frame_string = self._pprint_description_within_frame(vnframe, indent) + "\n" + frame_string += self._pprint_example_within_frame(vnframe, indent + " ") + "\n" + frame_string += ( + self._pprint_syntax_within_frame(vnframe, indent + " Syntax: ") + "\n" + ) + frame_string += indent + " Semantics:\n" + frame_string += self._pprint_semantics_within_frame(vnframe, indent + " ") + return frame_string + + def _pprint_example_within_frame(self, vnframe, indent=""): + """Returns pretty printed version of example within frame in a VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet frame example. + + :param vnframe: An ElementTree containing the xml contents of + a Verbnet frame. + """ + if vnframe["example"]: + return indent + " Example: " + vnframe["example"] + + def _pprint_description_within_frame(self, vnframe, indent=""): + """Returns pretty printed version of a VerbNet frame description + + Return a string containing a pretty-printed representation of + the given VerbNet frame description. + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + """ + description = indent + vnframe["description"]["primary"] + if vnframe["description"]["secondary"]: + description += " ({})".format(vnframe["description"]["secondary"]) + return description + + def _pprint_syntax_within_frame(self, vnframe, indent=""): + """Returns pretty printed version of syntax within a frame in a VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet frame syntax. + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + """ + pieces = [] + for element in vnframe["syntax"]: + piece = element["pos_tag"] + modifier_list = [] + if "value" in element["modifiers"] and element["modifiers"]["value"]: + modifier_list.append(element["modifiers"]["value"]) + modifier_list += [ + "{}{}".format(restr["value"], restr["type"]) + for restr in ( + element["modifiers"]["selrestrs"] + + element["modifiers"]["synrestrs"] + ) + ] + if modifier_list: + piece += "[{}]".format(" ".join(modifier_list)) + pieces.append(piece) + + return indent + " ".join(pieces) + + def _pprint_semantics_within_frame(self, vnframe, indent=""): + """Returns a pretty printed version of semantics within frame in a VerbNet class + + Return a string containing a pretty-printed representation of + the given VerbNet frame semantics. + + :param vnframe: An ElementTree containing the xml contents of + a VerbNet frame. + """ + pieces = [] + for predicate in vnframe["semantics"]: + arguments = [argument["value"] for argument in predicate["arguments"]] + pieces.append( + f"{'¬' if predicate['negated'] else ''}{predicate['predicate_value']}({', '.join(arguments)})" + ) + return "\n".join(f"{indent}* {piece}" for piece in pieces) diff --git a/nltk/corpus/reader/wordlist.py b/nltk/corpus/reader/wordlist.py new file mode 100644 index 0000000..365b7d4 --- /dev/null +++ b/nltk/corpus/reader/wordlist.py @@ -0,0 +1,182 @@ +# Natural Language Toolkit: Word List Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT +import os + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.util import * +from nltk.tokenize import line_tokenize + + +class WordListCorpusReader(CorpusReader): + """ + List of words, one per line. Blank lines are ignored. + """ + + def words(self, fileids=None, ignore_lines_startswith="\n", hf=False): + if hf: + from nltk.huggingface.dataset import load_data + + corpus_id = ( + self._root.corpus_id + if hasattr(self._root, "corpus_id") + else os.path.basename(self._root.path.rstrip("/")) + ) + content = load_data(corpus_id, fileid=fileids) + return [ + line + for line in content.splitlines() + if line and not line.startswith(ignore_lines_startswith) + ] + return [ + line + for line in line_tokenize(self.raw(fileids)) + if not line.startswith(ignore_lines_startswith) + ] + + +class SwadeshCorpusReader(WordListCorpusReader): + def entries(self, fileids=None): + """ + :return: a tuple of words for the specified fileids. + """ + if not fileids: + fileids = self.fileids() + + wordlists = [self.words(f) for f in fileids] + return list(zip(*wordlists)) + + +class NonbreakingPrefixesCorpusReader(WordListCorpusReader): + """ + This is a class to read the nonbreaking prefixes textfiles from the + Moses Machine Translation toolkit. These lists are used in the Python port + of the Moses' word tokenizer. + """ + + available_langs = { + "catalan": "ca", + "czech": "cs", + "german": "de", + "greek": "el", + "english": "en", + "spanish": "es", + "finnish": "fi", + "french": "fr", + "hungarian": "hu", + "icelandic": "is", + "italian": "it", + "latvian": "lv", + "dutch": "nl", + "polish": "pl", + "portuguese": "pt", + "romanian": "ro", + "russian": "ru", + "slovak": "sk", + "slovenian": "sl", + "swedish": "sv", + "tamil": "ta", + } + # Also, add the lang IDs as the keys. + available_langs.update({v: v for v in available_langs.values()}) + + def words(self, lang=None, fileids=None, ignore_lines_startswith="#"): + """ + This module returns a list of nonbreaking prefixes for the specified + language(s). + + >>> from nltk.corpus import nonbreaking_prefixes as nbp + >>> nbp.words('en')[:10] == [u'A', u'B', u'C', u'D', u'E', u'F', u'G', u'H', u'I', u'J'] + True + >>> nbp.words('ta')[:5] == [u'\u0b85', u'\u0b86', u'\u0b87', u'\u0b88', u'\u0b89'] + True + + :return: a list words for the specified language(s). + """ + # If *lang* in list of languages available, allocate apt fileid. + # Otherwise, the function returns non-breaking prefixes for + # all languages when fileids==None. + if lang in self.available_langs: + lang = self.available_langs[lang] + fileids = ["nonbreaking_prefix." + lang] + return [ + line + for line in line_tokenize(self.raw(fileids)) + if not line.startswith(ignore_lines_startswith) + ] + + +class UnicharsCorpusReader(WordListCorpusReader): + """ + This class is used to read lists of characters from the Perl Unicode + Properties (see https://perldoc.perl.org/perluniprops.html). + The files in the perluniprop.zip are extracted using the Unicode::Tussle + module from https://search.cpan.org/~bdfoy/Unicode-Tussle-1.11/lib/Unicode/Tussle.pm + """ + + # These are categories similar to the Perl Unicode Properties + available_categories = [ + "Close_Punctuation", + "Currency_Symbol", + "IsAlnum", + "IsAlpha", + "IsLower", + "IsN", + "IsSc", + "IsSo", + "IsUpper", + "Line_Separator", + "Number", + "Open_Punctuation", + "Punctuation", + "Separator", + "Symbol", + ] + + def chars(self, category=None, fileids=None): + """ + This module returns a list of characters from the Perl Unicode Properties. + They are very useful when porting Perl tokenizers to Python. + + >>> from nltk.corpus import perluniprops as pup + >>> pup.chars('Open_Punctuation')[:5] == [u'(', u'[', u'{', u'\u0f3a', u'\u0f3c'] + True + >>> pup.chars('Currency_Symbol')[:5] == [u'$', u'\xa2', u'\xa3', u'\xa4', u'\xa5'] + True + >>> pup.available_categories + ['Close_Punctuation', 'Currency_Symbol', 'IsAlnum', 'IsAlpha', 'IsLower', 'IsN', 'IsSc', 'IsSo', 'IsUpper', 'Line_Separator', 'Number', 'Open_Punctuation', 'Punctuation', 'Separator', 'Symbol'] + + :return: a list of characters given the specific unicode character category + """ + if category in self.available_categories: + fileids = [category + ".txt"] + return list(self.raw(fileids).strip()) + + +class MWAPPDBCorpusReader(WordListCorpusReader): + """ + This class is used to read the list of word pairs from the subset of lexical + pairs of The Paraphrase Database (PPDB) XXXL used in the Monolingual Word + Alignment (MWA) algorithm described in Sultan et al. (2014a, 2014b, 2015): + + - http://acl2014.org/acl2014/Q14/pdf/Q14-1017 + - https://www.aclweb.org/anthology/S14-2039 + - https://www.aclweb.org/anthology/S15-2027 + + The original source of the full PPDB corpus can be found on + https://www.cis.upenn.edu/~ccb/ppdb/ + + :return: a list of tuples of similar lexical terms. + """ + + mwa_ppdb_xxxl_file = "ppdb-1.0-xxxl-lexical.extended.synonyms.uniquepairs" + + def entries(self, fileids=mwa_ppdb_xxxl_file): + """ + :return: a tuple of synonym word pairs. + """ + return [tuple(line.split("\t")) for line in line_tokenize(self.raw(fileids))] diff --git a/nltk/corpus/reader/wordnet.py b/nltk/corpus/reader/wordnet.py new file mode 100644 index 0000000..d779d9b --- /dev/null +++ b/nltk/corpus/reader/wordnet.py @@ -0,0 +1,2567 @@ +# Natural Language Toolkit: WordNet +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bethard +# Steven Bird +# Edward Loper +# Nitin Madnani +# Nasruddin A’aidil Shari +# Sim Wei Ying Geraldine +# Soe Lynn +# Francis Bond +# Eric Kafe + +# URL: +# For license information, see LICENSE.TXT + +""" +An NLTK interface for WordNet + +WordNet is a lexical database of English. +Using synsets, helps find conceptual relationships between words +such as hypernyms, hyponyms, synonyms, antonyms etc. + +For details about WordNet see: +https://wordnet.princeton.edu/ + +This module also allows you to find lemmas in languages +other than English from the Open Multilingual Wordnet +https://omwn.org/ + +""" + +import math +import os +import re +import warnings +from collections import defaultdict, deque +from functools import total_ordering +from itertools import chain, islice +from operator import itemgetter + +from nltk.corpus.reader import CorpusReader +from nltk.internals import deprecated +from nltk.probability import FreqDist +from nltk.tag import map_tag +from nltk.util import binary_search_file as _binary_search_file + +###################################################################### +# Table of Contents +###################################################################### +# - Constants +# - Data Classes +# - WordNetError +# - Lemma +# - Synset +# - WordNet Corpus Reader +# - WordNet Information Content Corpus Reader +# - Similarity Metrics +# - Demo + +###################################################################### +# Constants +###################################################################### + +#: Positive infinity (for similarity functions) +_INF = 1e300 + +# { Part-of-speech constants +ADJ, ADJ_SAT, ADV, NOUN, VERB = "a", "s", "r", "n", "v" +# } + +POS_LIST = [NOUN, VERB, ADJ, ADV] + +# Convert from Universal Tags (Petrov et al., 2012) to Wordnet Pos +UNIVERSAL_TAG_TO_WN_POS = {"NOUN": "n", "VERB": "v", "ADJ": "a", "ADV": "r"} + +# A table of strings that are used to express verb frames. +VERB_FRAME_STRINGS = ( + None, + "Something %s", + "Somebody %s", + "It is %sing", + "Something is %sing PP", + "Something %s something Adjective/Noun", + "Something %s Adjective/Noun", + "Somebody %s Adjective", + "Somebody %s something", + "Somebody %s somebody", + "Something %s somebody", + "Something %s something", + "Something %s to somebody", + "Somebody %s on something", + "Somebody %s somebody something", + "Somebody %s something to somebody", + "Somebody %s something from somebody", + "Somebody %s somebody with something", + "Somebody %s somebody of something", + "Somebody %s something on somebody", + "Somebody %s somebody PP", + "Somebody %s something PP", + "Somebody %s PP", + "Somebody's (body part) %s", + "Somebody %s somebody to INFINITIVE", + "Somebody %s somebody INFINITIVE", + "Somebody %s that CLAUSE", + "Somebody %s to somebody", + "Somebody %s to INFINITIVE", + "Somebody %s whether INFINITIVE", + "Somebody %s somebody into V-ing something", + "Somebody %s something with something", + "Somebody %s INFINITIVE", + "Somebody %s VERB-ing", + "It %s that CLAUSE", + "Something %s INFINITIVE", + # OEWN additions: + "Somebody %s at something", + "Somebody %s for something", + "Somebody %s on somebody", + "Somebody %s out of somebody", +) + +SENSENUM_RE = re.compile(r"\.[\d]+\.") + + +###################################################################### +# Data Classes +###################################################################### + + +class WordNetError(Exception): + """An exception class for wordnet-related errors.""" + + +@total_ordering +class _WordNetObject: + """A common base class for lemmas and synsets.""" + + def hypernyms(self): + return self._related("@") + + def _hypernyms(self): + return self._related("@") + + def instance_hypernyms(self): + return self._related("@i") + + def _instance_hypernyms(self): + return self._related("@i") + + def hyponyms(self): + return self._related("~") + + def instance_hyponyms(self): + return self._related("~i") + + def member_holonyms(self): + return self._related("#m") + + def substance_holonyms(self): + return self._related("#s") + + def part_holonyms(self): + return self._related("#p") + + def member_meronyms(self): + return self._related("%m") + + def substance_meronyms(self): + return self._related("%s") + + def part_meronyms(self): + return self._related("%p") + + def topic_domains(self): + return self._related(";c") + + def in_topic_domains(self): + return self._related("-c") + + def region_domains(self): + return self._related(";r") + + def in_region_domains(self): + return self._related("-r") + + def usage_domains(self): + return self._related(";u") + + def in_usage_domains(self): + return self._related("-u") + + def attributes(self): + return self._related("=") + + def entailments(self): + return self._related("*") + + def causes(self): + return self._related(">") + + def also_sees(self): + return self._related("^") + + def verb_groups(self): + return self._related("$") + + def similar_tos(self): + return self._related("&") + + def __hash__(self): + return hash(self._name) + + def __eq__(self, other): + if not isinstance(other, _WordNetObject): + return NotImplemented + return self._name == other._name + + def __lt__(self, other): + if not isinstance(other, _WordNetObject): + return NotImplemented + return self._name < other._name + + +class Lemma(_WordNetObject): + """ + The lexical entry for a single morphological form of a + sense-disambiguated word. + + Create a Lemma from a "..." string where: + is the morphological stem identifying the synset + is one of the module attributes ADJ, ADJ_SAT, ADV, NOUN or VERB + is the sense number, counting from 0. + is the morphological form of interest + + Note that and can be different, e.g. the Synset + 'salt.n.03' has the Lemmas 'salt.n.03.salt', 'salt.n.03.saltiness' and + 'salt.n.03.salinity'. + + Lemma attributes, accessible via methods with the same name: + + - name: The canonical name of this lemma. + - synset: The synset that this lemma belongs to. + - syntactic_marker: For adjectives, the WordNet string identifying the + syntactic position relative modified noun. See: + https://wordnet.princeton.edu/documentation/wninput5wn + For all other parts of speech, this attribute is None. + - count: The frequency of this lemma in wordnet. + + Lemma methods: + + Lemmas have the following methods for retrieving related Lemmas. They + correspond to the names for the pointer symbols defined here: + https://wordnet.princeton.edu/documentation/wninput5wn + These methods all return lists of Lemmas: + + - antonyms + - hypernyms, instance_hypernyms + - hyponyms, instance_hyponyms + - member_holonyms, substance_holonyms, part_holonyms + - member_meronyms, substance_meronyms, part_meronyms + - topic_domains, region_domains, usage_domains + - attributes + - derivationally_related_forms + - entailments + - causes + - also_sees + - verb_groups + - similar_tos + - pertainyms + """ + + __slots__ = [ + "_wordnet_corpus_reader", + "_name", + "_syntactic_marker", + "_synset", + "_frame_strings", + "_frame_ids", + "_lexname_index", + "_lex_id", + "_lang", + "_key", + ] + + def __init__( + self, + wordnet_corpus_reader, + synset, + name, + lexname_index, + lex_id, + syntactic_marker, + ): + self._wordnet_corpus_reader = wordnet_corpus_reader + self._name = name + self._syntactic_marker = syntactic_marker + self._synset = synset + self._frame_strings = [] + self._frame_ids = [] + self._lexname_index = lexname_index + self._lex_id = lex_id + self._lang = "eng" + + self._key = None # gets set later. + + def name(self): + return self._name + + def syntactic_marker(self): + return self._syntactic_marker + + def synset(self): + return self._synset + + def frame_strings(self): + return self._frame_strings + + def frame_ids(self): + return self._frame_ids + + def lang(self): + return self._lang + + def key(self): + return self._key + + def __repr__(self): + tup = type(self).__name__, self._synset._name, self._name + return "%s('%s.%s')" % tup + + def _related(self, relation_symbol): + get_synset = self._wordnet_corpus_reader.synset_from_pos_and_offset + if (self._name, relation_symbol) not in self._synset._lemma_pointers: + return [] + return [ + get_synset(pos, offset)._lemmas[lemma_index] + for pos, offset, lemma_index in self._synset._lemma_pointers[ + self._name, relation_symbol + ] + ] + + def count(self): + """Return the frequency count for this Lemma""" + return self._wordnet_corpus_reader.lemma_count(self) + + def antonyms(self): + return self._related("!") + + def derivationally_related_forms(self): + return self._related("+") + + def pertainyms(self): + return self._related("\\") + + +class Synset(_WordNetObject): + """Create a Synset from a ".." string where: + is the word's morphological stem + is one of the module attributes ADJ, ADJ_SAT, ADV, NOUN or VERB + is the sense number, counting from 0. + + Synset attributes, accessible via methods with the same name: + + - name: The canonical name of this synset, formed using the first lemma + of this synset. Note that this may be different from the name + passed to the constructor if that string used a different lemma to + identify the synset. + - pos: The synset's part of speech, matching one of the module level + attributes ADJ, ADJ_SAT, ADV, NOUN or VERB. + - lemmas: A list of the Lemma objects for this synset. + - definition: The definition for this synset. + - examples: A list of example strings for this synset. + - offset: The offset in the WordNet dict file of this synset. + - lexname: The name of the lexicographer file containing this synset. + + Synset methods: + + Synsets have the following methods for retrieving related Synsets. + They correspond to the names for the pointer symbols defined here: + https://wordnet.princeton.edu/documentation/wninput5wn + These methods all return lists of Synsets. + + - hypernyms, instance_hypernyms + - hyponyms, instance_hyponyms + - member_holonyms, substance_holonyms, part_holonyms + - member_meronyms, substance_meronyms, part_meronyms + - attributes + - entailments + - causes + - also_sees + - verb_groups + - similar_tos + + Additionally, Synsets support the following methods specific to the + hypernym relation: + + - root_hypernyms + - common_hypernyms + - lowest_common_hypernyms + + Note that Synsets do not support the following relations because + these are defined by WordNet as lexical relations: + + - antonyms + - derivationally_related_forms + - pertainyms + """ + + __slots__ = [ + "_pos", + "_offset", + "_name", + "_frame_ids", + "_lemmas", + "_lemma_names", + "_definition", + "_examples", + "_lexname", + "_pointers", + "_lemma_pointers", + "_max_depth", + "_min_depth", + ] + + def __init__(self, wordnet_corpus_reader): + self._wordnet_corpus_reader = wordnet_corpus_reader + # All of these attributes get initialized by + # WordNetCorpusReader._synset_from_pos_and_line() + + self._pos = None + self._offset = None + self._name = None + self._frame_ids = [] + self._lemmas = [] + self._lemma_names = [] + self._definition = None + self._examples = [] + self._lexname = None # lexicographer name + self._all_hypernyms = None + + self._pointers = defaultdict(set) + self._lemma_pointers = defaultdict(list) + + def pos(self): + return self._pos + + def offset(self): + return self._offset + + def name(self): + return self._name + + def frame_ids(self): + return self._frame_ids + + def _doc(self, doc_type, default, lang="eng"): + """Helper method for Synset.definition and Synset.examples""" + corpus = self._wordnet_corpus_reader + if lang not in corpus.langs(): + return None + elif lang == "eng": + return default + else: + corpus._load_lang_data(lang) + of = corpus.ss2of(self) + i = corpus.lg_attrs.index(doc_type) + if of in corpus._lang_data[lang][i]: + return corpus._lang_data[lang][i][of] + else: + return None + + def definition(self, lang="eng"): + """Return definition in specified language""" + return self._doc("def", self._definition, lang=lang) + + def examples(self, lang="eng"): + """Return examples in specified language""" + return self._doc("exe", self._examples, lang=lang) + + def lexname(self): + return self._lexname + + def _needs_root(self): + if self._pos == NOUN and self._wordnet_corpus_reader.get_version() != "1.6": + return False + else: + return True + + def lemma_names(self, lang="eng"): + """Return all the lemma_names associated with the synset""" + if lang == "eng": + return self._lemma_names + else: + reader = self._wordnet_corpus_reader + reader._load_lang_data(lang) + i = reader.ss2of(self) + if i in reader._lang_data[lang][0]: + return reader._lang_data[lang][0][i] + else: + return [] + + def lemmas(self, lang="eng"): + """Return all the lemma objects associated with the synset""" + if lang == "eng": + return self._lemmas + elif self._name: + self._wordnet_corpus_reader._load_lang_data(lang) + lemmark = [] + lemmy = self.lemma_names(lang) + for lem in lemmy: + temp = Lemma( + self._wordnet_corpus_reader, + self, + lem, + self._wordnet_corpus_reader._lexnames.index(self.lexname()), + 0, + None, + ) + temp._lang = lang + lemmark.append(temp) + return lemmark + + def root_hypernyms(self): + """Get the topmost hypernyms of this synset in WordNet.""" + + result = [] + seen = set() + todo = [self] + while todo: + next_synset = todo.pop() + if next_synset not in seen: + seen.add(next_synset) + next_hypernyms = ( + next_synset.hypernyms() + next_synset.instance_hypernyms() + ) + if not next_hypernyms: + result.append(next_synset) + else: + todo.extend(next_hypernyms) + return result + + # Simpler implementation which makes incorrect assumption that + # hypernym hierarchy is acyclic: + # + # if not self.hypernyms(): + # return [self] + # else: + # return list(set(root for h in self.hypernyms() + # for root in h.root_hypernyms())) + def max_depth(self): + """ + :return: The length of the longest hypernym path from this + synset to the root. + """ + + if "_max_depth" not in self.__dict__: + hypernyms = self.hypernyms() + self.instance_hypernyms() + if not hypernyms: + self._max_depth = 0 + else: + self._max_depth = 1 + max(h.max_depth() for h in hypernyms) + return self._max_depth + + def min_depth(self): + """ + :return: The length of the shortest hypernym path from this + synset to the root. + """ + + if "_min_depth" not in self.__dict__: + hypernyms = self.hypernyms() + self.instance_hypernyms() + if not hypernyms: + self._min_depth = 0 + else: + self._min_depth = 1 + min(h.min_depth() for h in hypernyms) + return self._min_depth + + def closure(self, rel, depth=-1): + """ + Return the transitive closure of source under the rel + relationship, breadth-first, discarding cycles: + + >>> from nltk.corpus import wordnet as wn + >>> computer = wn.synset('computer.n.01') + >>> topic = lambda s:s.topic_domains() + >>> print(list(computer.closure(topic))) + [Synset('computer_science.n.01')] + + UserWarning: Discarded redundant search for Synset('computer.n.01') at depth 2 + + + Include redundant paths (but only once), avoiding duplicate searches + (from 'animal.n.01' to 'entity.n.01'): + + >>> dog = wn.synset('dog.n.01') + >>> hyp = lambda s:sorted(s.hypernyms()) + >>> print(list(dog.closure(hyp))) + [Synset('canine.n.02'), Synset('domestic_animal.n.01'), Synset('carnivore.n.01'),\ + Synset('animal.n.01'), Synset('placental.n.01'), Synset('organism.n.01'),\ + Synset('mammal.n.01'), Synset('living_thing.n.01'), Synset('vertebrate.n.01'),\ + Synset('whole.n.02'), Synset('chordate.n.01'), Synset('object.n.01'),\ + Synset('physical_entity.n.01'), Synset('entity.n.01')] + + UserWarning: Discarded redundant search for Synset('animal.n.01') at depth 7 + """ + + from nltk.util import acyclic_breadth_first + + for synset in acyclic_breadth_first(self, rel, depth): + if synset != self: + yield synset + + from nltk.util import acyclic_depth_first as acyclic_tree + from nltk.util import unweighted_minimum_spanning_tree as mst + + # Also add this shortcut? + # from nltk.util import unweighted_minimum_spanning_digraph as umsd + + def tree(self, rel, depth=-1, cut_mark=None): + """ + Return the full relation tree, including self, + discarding cycles: + + >>> from nltk.corpus import wordnet as wn + >>> from pprint import pprint + >>> computer = wn.synset('computer.n.01') + >>> topic = lambda s:sorted(s.topic_domains()) + >>> pprint(computer.tree(topic)) + [Synset('computer.n.01'), [Synset('computer_science.n.01')]] + + UserWarning: Discarded redundant search for Synset('computer.n.01') at depth -3 + + + But keep duplicate branches (from 'animal.n.01' to 'entity.n.01'): + + >>> dog = wn.synset('dog.n.01') + >>> hyp = lambda s:sorted(s.hypernyms()) + >>> pprint(dog.tree(hyp)) + [Synset('dog.n.01'), + [Synset('canine.n.02'), + [Synset('carnivore.n.01'), + [Synset('placental.n.01'), + [Synset('mammal.n.01'), + [Synset('vertebrate.n.01'), + [Synset('chordate.n.01'), + [Synset('animal.n.01'), + [Synset('organism.n.01'), + [Synset('living_thing.n.01'), + [Synset('whole.n.02'), + [Synset('object.n.01'), + [Synset('physical_entity.n.01'), + [Synset('entity.n.01')]]]]]]]]]]]]], + [Synset('domestic_animal.n.01'), + [Synset('animal.n.01'), + [Synset('organism.n.01'), + [Synset('living_thing.n.01'), + [Synset('whole.n.02'), + [Synset('object.n.01'), + [Synset('physical_entity.n.01'), [Synset('entity.n.01')]]]]]]]]] + """ + + from nltk.util import acyclic_branches_depth_first + + return acyclic_branches_depth_first(self, rel, depth, cut_mark) + + def hypernym_paths(self): + """ + Get the path(s) from this synset to the root, where each path is a + list of the synset nodes traversed on the way to the root. + + :return: A list of lists, where each list gives the node sequence + connecting the initial ``Synset`` node and a root node. + """ + paths = [] + + hypernyms = self.hypernyms() + self.instance_hypernyms() + if len(hypernyms) == 0: + paths = [[self]] + + for hypernym in hypernyms: + for ancestor_list in hypernym.hypernym_paths(): + ancestor_list.append(self) + paths.append(ancestor_list) + return paths + + def common_hypernyms(self, other): + """ + Find all synsets that are hypernyms of this synset and the + other synset. + + :type other: Synset + :param other: other input synset. + :return: The synsets that are hypernyms of both synsets. + """ + if not self._all_hypernyms: + self._all_hypernyms = { + self_synset + for self_synsets in self._iter_hypernym_lists() + for self_synset in self_synsets + } + if not other._all_hypernyms: + other._all_hypernyms = { + other_synset + for other_synsets in other._iter_hypernym_lists() + for other_synset in other_synsets + } + return list(self._all_hypernyms.intersection(other._all_hypernyms)) + + def lowest_common_hypernyms(self, other, simulate_root=False, use_min_depth=False): + """ + Get a list of lowest synset(s) that both synsets have as a hypernym. + When `use_min_depth == False` this means that the synset which appears + as a hypernym of both `self` and `other` with the lowest maximum depth + is returned or if there are multiple such synsets at the same depth + they are all returned + + However, if `use_min_depth == True` then the synset(s) which has/have + the lowest minimum depth and appear(s) in both paths is/are returned. + + By setting the use_min_depth flag to True, the behavior of NLTK2 can be + preserved. This was changed in NLTK3 to give more accurate results in a + small set of cases, generally with synsets concerning people. (eg: + 'chef.n.01', 'fireman.n.01', etc.) + + This method is an implementation of Ted Pedersen's "Lowest Common + Subsumer" method from the Perl Wordnet module. It can return either + "self" or "other" if they are a hypernym of the other. + + :type other: Synset + :param other: other input synset + :type simulate_root: bool + :param simulate_root: The various verb taxonomies do not + share a single root which disallows this metric from working for + synsets that are not connected. This flag (False by default) + creates a fake root that connects all the taxonomies. Set it + to True to enable this behavior. For the noun taxonomy, + there is usually a default root except for WordNet version 1.6. + If you are using wordnet 1.6, a fake root will need to be added + for nouns as well. + :type use_min_depth: bool + :param use_min_depth: This setting mimics older (v2) behavior of NLTK + wordnet If True, will use the min_depth function to calculate the + lowest common hypernyms. This is known to give strange results for + some synset pairs (eg: 'chef.n.01', 'fireman.n.01') but is retained + for backwards compatibility + :return: The synsets that are the lowest common hypernyms of both + synsets + """ + synsets = self.common_hypernyms(other) + if simulate_root: + fake_synset = Synset(None) + fake_synset._name = "*ROOT*" + fake_synset.hypernyms = lambda: [] + fake_synset.instance_hypernyms = lambda: [] + synsets.append(fake_synset) + + try: + if use_min_depth: + max_depth = max(s.min_depth() for s in synsets) + unsorted_lch = [s for s in synsets if s.min_depth() == max_depth] + else: + max_depth = max(s.max_depth() for s in synsets) + unsorted_lch = [s for s in synsets if s.max_depth() == max_depth] + return sorted(unsorted_lch) + except ValueError: + return [] + + def hypernym_distances(self, distance=0, simulate_root=False): + """ + Get the path(s) from this synset to the root, counting the distance + of each node from the initial node on the way. A set of + (synset, distance) tuples is returned. + + :type distance: int + :param distance: the distance (number of edges) from this hypernym to + the original hypernym ``Synset`` on which this method was called. + :return: A set of ``(Synset, int)`` tuples where each ``Synset`` is + a hypernym of the first ``Synset``. + """ + distances = {(self, distance)} + for hypernym in self._hypernyms() + self._instance_hypernyms(): + distances |= hypernym.hypernym_distances(distance + 1, simulate_root=False) + if simulate_root: + fake_synset = Synset(None) + fake_synset._name = "*ROOT*" + fake_synset_distance = max(distances, key=itemgetter(1))[1] + distances.add((fake_synset, fake_synset_distance + 1)) + return distances + + def _shortest_hypernym_paths(self, simulate_root): + if self._name == "*ROOT*": + return {self: 0} + + queue = deque([(self, 0)]) + path = {} + + while queue: + s, depth = queue.popleft() + if s in path: + continue + path[s] = depth + + depth += 1 + queue.extend((hyp, depth) for hyp in s._hypernyms()) + queue.extend((hyp, depth) for hyp in s._instance_hypernyms()) + + if simulate_root: + fake_synset = Synset(None) + fake_synset._name = "*ROOT*" + path[fake_synset] = max(path.values()) + 1 + + return path + + def shortest_path_distance(self, other, simulate_root=False): + """ + Returns the distance of the shortest path linking the two synsets (if + one exists). For each synset, all the ancestor nodes and their + distances are recorded and compared. The ancestor node common to both + synsets that can be reached with the minimum number of traversals is + used. If no ancestor nodes are common, None is returned. If a node is + compared with itself 0 is returned. + + :type other: Synset + :param other: The Synset to which the shortest path will be found. + :return: The number of edges in the shortest path connecting the two + nodes, or None if no path exists. + """ + + if self == other: + return 0 + + dist_dict1 = self._shortest_hypernym_paths(simulate_root) + dist_dict2 = other._shortest_hypernym_paths(simulate_root) + + # For each ancestor synset common to both subject synsets, find the + # connecting path length. Return the shortest of these. + + inf = float("inf") + path_distance = inf + for synset, d1 in dist_dict1.items(): + d2 = dist_dict2.get(synset, inf) + path_distance = min(path_distance, d1 + d2) + + return None if math.isinf(path_distance) else path_distance + + # interface to similarity methods + def path_similarity(self, other, verbose=False, simulate_root=True): + """ + Path Distance Similarity: + Return a score denoting how similar two word senses are, based on the + shortest path that connects the senses in the is-a (hypernym/hypnoym) + taxonomy. The score is in the range 0 to 1, except in those cases where + a path cannot be found (will only be true for verbs as there are many + distinct verb taxonomies), in which case None is returned. A score of + 1 represents identity i.e. comparing a sense with itself will return 1. + + :type other: Synset + :param other: The ``Synset`` that this ``Synset`` is being compared to. + :type simulate_root: bool + :param simulate_root: The various verb taxonomies do not + share a single root which disallows this metric from working for + synsets that are not connected. This flag (True by default) + creates a fake root that connects all the taxonomies. Set it + to false to disable this behavior. For the noun taxonomy, + there is usually a default root except for WordNet version 1.6. + If you are using wordnet 1.6, a fake root will be added for nouns + as well. + :return: A score denoting the similarity of the two ``Synset`` objects, + normally between 0 and 1. None is returned if no connecting path + could be found. 1 is returned if a ``Synset`` is compared with + itself. + """ + + distance = self.shortest_path_distance( + other, + simulate_root=simulate_root and (self._needs_root() or other._needs_root()), + ) + if distance is None or distance < 0: + return None + return 1.0 / (distance + 1) + + def lch_similarity(self, other, verbose=False, simulate_root=True): + """ + Leacock Chodorow Similarity: + Return a score denoting how similar two word senses are, based on the + shortest path that connects the senses (as above) and the maximum depth + of the taxonomy in which the senses occur. The relationship is given as + -log(p/2d) where p is the shortest path length and d is the taxonomy + depth. + + :type other: Synset + :param other: The ``Synset`` that this ``Synset`` is being compared to. + :type simulate_root: bool + :param simulate_root: The various verb taxonomies do not + share a single root which disallows this metric from working for + synsets that are not connected. This flag (True by default) + creates a fake root that connects all the taxonomies. Set it + to false to disable this behavior. For the noun taxonomy, + there is usually a default root except for WordNet version 1.6. + If you are using wordnet 1.6, a fake root will be added for nouns + as well. + :return: A score denoting the similarity of the two ``Synset`` objects, + normally greater than 0. None is returned if no connecting path + could be found. If a ``Synset`` is compared with itself, the + maximum score is returned, which varies depending on the taxonomy + depth. + """ + + if self._pos != other._pos: + raise WordNetError( + "Computing the lch similarity requires " + "%s and %s to have the same part of speech." % (self, other) + ) + + need_root = self._needs_root() + key = (self._pos, need_root) + + if key not in self._wordnet_corpus_reader._max_depth: + self._wordnet_corpus_reader._compute_max_depth(self._pos, need_root) + + depth = self._wordnet_corpus_reader._max_depth[key] + + distance = self.shortest_path_distance( + other, simulate_root=simulate_root and need_root + ) + + if distance is None or distance < 0 or depth == 0: + return None + return -math.log((distance + 1) / (2.0 * depth)) + + def wup_similarity(self, other, verbose=False, simulate_root=True): + """ + Wu-Palmer Similarity: + Return a score denoting how similar two word senses are, based on the + depth of the two senses in the taxonomy and that of their Least Common + Subsumer (most specific ancestor node). Previously, the scores computed + by this implementation did _not_ always agree with those given by + Pedersen's Perl implementation of WordNet Similarity. However, with + the addition of the simulate_root flag (see below), the score for + verbs now almost always agree but not always for nouns. + + The LCS does not necessarily feature in the shortest path connecting + the two senses, as it is by definition the common ancestor deepest in + the taxonomy, not closest to the two senses. Typically, however, it + will so feature. Where multiple candidates for the LCS exist, that + whose shortest path to the root node is the longest will be selected. + Where the LCS has multiple paths to the root, the longer path is used + for the purposes of the calculation. + + :type other: Synset + :param other: The ``Synset`` that this ``Synset`` is being compared to. + :type simulate_root: bool + :param simulate_root: The various verb taxonomies do not + share a single root which disallows this metric from working for + synsets that are not connected. This flag (True by default) + creates a fake root that connects all the taxonomies. Set it + to false to disable this behavior. For the noun taxonomy, + there is usually a default root except for WordNet version 1.6. + If you are using wordnet 1.6, a fake root will be added for nouns + as well. + :return: A float score denoting the similarity of the two ``Synset`` + objects, normally greater than zero. If no connecting path between + the two senses can be found, None is returned. + + """ + need_root = self._needs_root() or other._needs_root() + + # Note that to preserve behavior from NLTK2 we set use_min_depth=True + # It is possible that more accurate results could be obtained by + # removing this setting and it should be tested later on + subsumers = self.lowest_common_hypernyms( + other, simulate_root=simulate_root and need_root, use_min_depth=True + ) + + # If no LCS was found return None + if len(subsumers) == 0: + return None + + subsumer = self if self in subsumers else subsumers[0] + + # Get the longest path from the LCS to the root, + # including a correction: + # - add one because the calculations include both the start and end + # nodes + depth = subsumer.max_depth() + 1 + + # Note: No need for an additional add-one correction for non-nouns + # to account for an imaginary root node because that is now + # automatically handled by simulate_root + # if subsumer._pos != NOUN: + # depth += 1 + + # Get the shortest path from the LCS to each of the synsets it is + # subsuming. Add this to the LCS path length to get the path + # length from each synset to the root. + len1 = self.shortest_path_distance( + subsumer, simulate_root=simulate_root and need_root + ) + len2 = other.shortest_path_distance( + subsumer, simulate_root=simulate_root and need_root + ) + if len1 is None or len2 is None: + return None + len1 += depth + len2 += depth + return (2.0 * depth) / (len1 + len2) + + def res_similarity(self, other, ic, verbose=False): + """ + Resnik Similarity: + Return a score denoting how similar two word senses are, based on the + Information Content (IC) of the Least Common Subsumer (most specific + ancestor node). + + :type other: Synset + :param other: The ``Synset`` that this ``Synset`` is being compared to. + :type ic: dict + :param ic: an information content object (as returned by + ``nltk.corpus.wordnet_ic.ic()``). + :return: A float score denoting the similarity of the two ``Synset`` + objects. Synsets whose LCS is the root node of the taxonomy will + have a score of 0 (e.g. N['dog'][0] and N['table'][0]). + """ + + ic1, ic2, lcs_ic = _lcs_ic(self, other, ic) + return lcs_ic + + def jcn_similarity(self, other, ic, verbose=False): + """ + Jiang-Conrath Similarity: + Return a score denoting how similar two word senses are, based on the + Information Content (IC) of the Least Common Subsumer (most specific + ancestor node) and that of the two input Synsets. The relationship is + given by the equation 1 / (IC(s1) + IC(s2) - 2 * IC(lcs)). + + :type other: Synset + :param other: The ``Synset`` that this ``Synset`` is being compared to. + :type ic: dict + :param ic: an information content object (as returned by + ``nltk.corpus.wordnet_ic.ic()``). + :return: A float score denoting the similarity of the two ``Synset`` + objects. + """ + + if self == other: + return _INF + + ic1, ic2, lcs_ic = _lcs_ic(self, other, ic) + + # If either of the input synsets are the root synset, or have a + # frequency of 0 (sparse data problem), return 0. + if ic1 == 0 or ic2 == 0: + return 0 + + ic_difference = ic1 + ic2 - 2 * lcs_ic + + if ic_difference == 0: + return _INF + + return 1 / ic_difference + + def lin_similarity(self, other, ic, verbose=False): + """ + Lin Similarity: + Return a score denoting how similar two word senses are, based on the + Information Content (IC) of the Least Common Subsumer (most specific + ancestor node) and that of the two input Synsets. The relationship is + given by the equation 2 * IC(lcs) / (IC(s1) + IC(s2)). + + :type other: Synset + :param other: The ``Synset`` that this ``Synset`` is being compared to. + :type ic: dict + :param ic: an information content object (as returned by + ``nltk.corpus.wordnet_ic.ic()``). + :return: A float score denoting the similarity of the two ``Synset`` + objects, in the range 0 to 1. + """ + + ic1, ic2, lcs_ic = _lcs_ic(self, other, ic) + return (2.0 * lcs_ic) / (ic1 + ic2) + + def _iter_hypernym_lists(self): + """ + :return: An iterator over ``Synset`` objects that are either proper + hypernyms or instance of hypernyms of the synset. + """ + todo = [self] + seen = set() + while todo: + for synset in todo: + seen.add(synset) + yield todo + todo = [ + hypernym + for synset in todo + for hypernym in (synset.hypernyms() + synset.instance_hypernyms()) + if hypernym not in seen + ] + + def __repr__(self): + return f"{type(self).__name__}('{self._name}')" + + def _related(self, relation_symbol): + get_synset = self._wordnet_corpus_reader.synset_from_pos_and_offset + if relation_symbol not in self._pointers: + return [] + pointer_tuples = self._pointers[relation_symbol] + r = [get_synset(pos, offset) for pos, offset in pointer_tuples] + return r + + +###################################################################### +# WordNet Corpus Reader +###################################################################### + + +class WordNetCorpusReader(CorpusReader): + """ + A corpus reader used to access wordnet or its variants. + """ + + _ENCODING = "utf8" + + # { Part-of-speech constants + ADJ, ADJ_SAT, ADV, NOUN, VERB = "a", "s", "r", "n", "v" + # } + + # { Filename constants + _FILEMAP = {ADJ: "adj", ADV: "adv", NOUN: "noun", VERB: "verb"} + # } + + # { Part of speech constants + _pos_numbers = {NOUN: 1, VERB: 2, ADJ: 3, ADV: 4, ADJ_SAT: 5} + _pos_names = dict(tup[::-1] for tup in _pos_numbers.items()) + # } + + #: A list of file identifiers for all the fileids used by this + #: corpus reader. + _FILES = ( + "cntlist.rev", + "lexnames", + "index.sense", + "index.adj", + "index.adv", + "index.noun", + "index.verb", + "data.adj", + "data.adv", + "data.noun", + "data.verb", + "adj.exc", + "adv.exc", + "noun.exc", + "verb.exc", + ) + + def __init__(self, root, omw_reader): + """ + Construct a new wordnet corpus reader, with the given root + directory. + """ + + super().__init__(root, self._FILES, encoding=self._ENCODING) + + # A index that provides the file offset + # Map from lemma -> pos -> synset_index -> offset + self._lemma_pos_offset_map = defaultdict(dict) + + # A cache so we don't have to reconstruct synsets + # Map from pos -> offset -> synset + self._synset_offset_cache = defaultdict(dict) + + # A cache for the maximum depth used by the lch similarity metric. + # Keyed by (pos, need_root), where need_root indicates whether a simulated root is required.. + self._max_depth = {} + + # Corpus reader containing omw data. + self._omw_reader = omw_reader + + # Corpus reader containing extended_omw data. + self._exomw_reader = None + + self.provenances = defaultdict(str) + self.provenances["eng"] = "" + + if self._omw_reader is None: + warnings.warn( + "The multilingual functions are not available with this Wordnet version" + ) + + self.omw_langs = set() + + # A cache to store the wordnet data of multiple languages + self._lang_data = defaultdict(list) + + self._data_file_map = {} + self._exception_map = {} + self._lexnames = [] + self._key_count_file = None + self._key_synset_file = None + + # Load the lexnames + with self.open("lexnames") as fp: + for i, line in enumerate(fp): + index, lexname, _ = line.split() + assert int(index) == i + self._lexnames.append(lexname) + + # Build a set of adjective satellite offsets + self._scan_satellites() + + # Load the indices for lemmas and synset offsets + self._load_lemma_pos_offset_map() + + # load the exception file data into memory + self._load_exception_map() + + self.nomap = {} + self.splits = {} + self.merges = {} + + # map from WordNet 3.0 for OMW data + self.map30 = self.map_wn() + + # Language data attributes + self.lg_attrs = ["lemma", "of", "def", "exe"] + + def index_sense(self, version=None): + """Read sense key to synset id mapping from index.sense file in corpus directory""" + fn = "index.sense" + if version: + from nltk.corpus import CorpusReader, LazyCorpusLoader + + ixreader = LazyCorpusLoader(version, CorpusReader, r".*/" + fn) + else: + ixreader = self + with ixreader.open(fn) as fp: + sensekey_map = {} + for line in fp: + fields = line.strip().split() + sensekey = fields[0] + pos = self._pos_names[int(sensekey.split("%")[1].split(":")[0])] + sensekey_map[sensekey] = f"{fields[1]}-{pos}" + return sensekey_map + + def map_to_many(self, version="wordnet"): + sensekey_map1 = self.index_sense(version) + sensekey_map2 = self.index_sense() + synset_to_many = {} + for synsetid in set(sensekey_map1.values()): + synset_to_many[synsetid] = [] + for sensekey in set(sensekey_map1.keys()).intersection( + set(sensekey_map2.keys()) + ): + source = sensekey_map1[sensekey] + target = sensekey_map2[sensekey] + synset_to_many[source].append(target) + return synset_to_many + + def map_to_one(self, version="wordnet"): + self.nomap[version] = set() + self.splits[version] = {} + synset_to_many = self.map_to_many(version) + synset_to_one = {} + for source in synset_to_many: + candidates_bag = synset_to_many[source] + if candidates_bag: + candidates_set = set(candidates_bag) + if len(candidates_set) == 1: + target = candidates_bag[0] + else: + counts = [] + for candidate in candidates_set: + counts.append((candidates_bag.count(candidate), candidate)) + self.splits[version][source] = counts + target = max(counts)[1] + synset_to_one[source] = target + if source[-1] == "s": + # Add a mapping from "a" to target for applications like omw, + # where only Lithuanian and Slovak use the "s" ss_type. + synset_to_one[f"{source[:-1]}a"] = target + else: + self.nomap[version].add(source) + return synset_to_one + + def map_wn(self, version="wordnet"): + """Mapping from Wordnet 'version' to currently loaded Wordnet version""" + if self.get_version() == version: + return None + else: + return self.map_to_one(version) + + def split_synsets(self, version="wordnet"): + if version not in self.splits: + _mymap = self.map_to_one(version) + return self.splits[version] + + def merged_synsets(self, version="wordnet"): + if version not in self.merges: + merge = defaultdict(set) + for source, targets in self.map_to_many(version).items(): + for target in targets: + merge[target].add(source) + self.merges[version] = { + trg: src for trg, src in merge.items() if len(src) > 1 + } + return self.merges[version] + + # Open Multilingual WordNet functions, contributed by + # Nasruddin A’aidil Shari, Sim Wei Ying Geraldine, and Soe Lynn + + def of2ss(self, of): + """take an id and return the synsets""" + return self.synset_from_pos_and_offset(of[-1], int(of[:8])) + + def ss2of(self, ss): + """return the ID of the synset""" + if ss: + return f"{ss.offset():08d}-{ss.pos()}" + + def _load_lang_data(self, lang): + """load the wordnet data of the requested language from the file to + the cache, _lang_data""" + + if lang in self._lang_data: + return + + if self._omw_reader and not self.omw_langs: + self.add_omw() + + if lang not in self.langs(): + raise WordNetError(f"Language {lang} is not supported.") + + if self._exomw_reader and lang not in self.omw_langs: + reader = self._exomw_reader + else: + reader = self._omw_reader + + prov = self.provenances[lang] + if prov in ["cldr", "wikt"]: + prov2 = prov + else: + prov2 = "data" + + with reader.open(f"{prov}/wn-{prov2}-{lang.split('_')[0]}.tab") as fp: + self.custom_lemmas(fp, lang) + self.disable_custom_lemmas(lang) + + def add_provs(self, reader): + """Add languages from Multilingual Wordnet to the provenance dictionary""" + fileids = reader.fileids() + for fileid in fileids: + prov, langfile = os.path.split(fileid) + file_name, file_extension = os.path.splitext(langfile) + if file_extension == ".tab": + lang = file_name.split("-")[-1] + if lang in self.provenances or prov in ["cldr", "wikt"]: + # We already have another resource for this lang, + # so we need to further specify the lang id: + lang = f"{lang}_{prov}" + self.provenances[lang] = prov + + def add_omw(self): + self.add_provs(self._omw_reader) + self.omw_langs = set(self.provenances.keys()) + + def add_exomw(self): + """ + Add languages from Extended OMW + + >>> import nltk + >>> from nltk.corpus import wordnet as wn + >>> wn.add_exomw() + >>> print(wn.synset('intrinsically.r.01').lemmas(lang="eng_wikt")) + [Lemma('intrinsically.r.01.per_se'), Lemma('intrinsically.r.01.as_such')] + """ + from nltk.corpus import extended_omw + + self.add_omw() + self._exomw_reader = extended_omw + self.add_provs(self._exomw_reader) + + def langs(self): + """return a list of languages supported by Multilingual Wordnet""" + return list(self.provenances.keys()) + + def _scan_satellites(self): + """ + Scans the adjective data file and populates self.satellite_offsets with all adjective satellite synset offsets. + + This method reads the adjective data file associated with the corpus reader, + identifies synsets of type 's' (adjective satellites), and adds their offsets + to the self.satellite_offsets set. The method does not return a value. + """ + adj_data_file = self._data_file(ADJ) + satellite_offsets = set() + adj_data_file.seek(0) + for line in adj_data_file: + if not line.strip() or line.startswith(" "): + continue + fields = line.strip().split() + if len(fields) < 3: + continue + synset_offset = fields[0] + synset_type = fields[2] + if synset_type == "s": + satellite_offsets.add(int(synset_offset)) + adj_data_file.seek(0) # Reset if needed elsewhere + self.satellite_offsets = satellite_offsets + + def _load_lemma_pos_offset_map(self): + for suffix in self._FILEMAP.values(): + # parse each line of the file (ignoring comment lines) + with self.open("index.%s" % suffix) as fp: + for i, line in enumerate(fp): + if line.startswith(" "): + continue + + _iter = iter(line.split()) + + def _next_token(): + return next(_iter) + + try: + # get the lemma and part-of-speech + lemma = _next_token() + pos = _next_token() + + # get the number of synsets for this lemma + n_synsets = int(_next_token()) + assert n_synsets > 0 + + # get and ignore the pointer symbols for all synsets of + # this lemma + n_pointers = int(_next_token()) + [_next_token() for _ in range(n_pointers)] + + # same as number of synsets + n_senses = int(_next_token()) + assert n_synsets == n_senses + + # get and ignore number of senses ranked according to + # frequency + _next_token() + + # get synset offsets + synset_offsets = [int(_next_token()) for _ in range(n_synsets)] + + # raise more informative error with file name and line number + except (AssertionError, ValueError) as e: + tup = ("index.%s" % suffix), (i + 1), e + raise WordNetError("file %s, line %i: %s" % tup) from e + + # map lemmas and parts of speech to synsets + self._lemma_pos_offset_map[lemma][pos] = synset_offsets + if pos == ADJ: + # index.adj uses only the ADJ pos, so identify ADJ_SAT using satellites set + satellite_offsets = [ + # Keep the ordering from index.adj + offset + for offset in synset_offsets + if offset in self.satellite_offsets + ] + # Duplicate only a (possibly empty) list of real satellites + self._lemma_pos_offset_map[lemma][ADJ_SAT] = satellite_offsets + + def _load_exception_map(self): + # load the exception file data into memory + for pos, suffix in self._FILEMAP.items(): + self._exception_map[pos] = {} + with self.open("%s.exc" % suffix) as fp: + for line in fp: + terms = line.split() + self._exception_map[pos][terms[0]] = terms[1:] + self._exception_map[ADJ_SAT] = self._exception_map[ADJ] + + def _compute_max_depth(self, pos, simulate_root): + """ + Compute the max depth for the given part of speech. This is + used by the lch similarity metric. + """ + key = (pos, simulate_root) + if key in self._max_depth: + return self._max_depth[key] + + depth = 0 + for ss in self.all_synsets(pos): + try: + depth = max(depth, ss.max_depth()) + except RuntimeError: + print(ss) + + if simulate_root: + depth += 1 + + self._max_depth[key] = depth + return depth + + def get_version(self): + fh = self._data_file(ADJ) + fh.seek(0) + for line in fh: + match = re.search(r"Word[nN]et (\d+\+?|\d+\.\d+) Copyright", line) + if match is not None: + version = match.group(1) + fh.seek(0) + return version + + ############################################################# + # Loading Lemmas + ############################################################# + + def lemma(self, name, lang="eng"): + """Return lemma object that matches the name""" + # cannot simply split on first '.', + # e.g.: '.45_caliber.a.01..45_caliber' + separator = SENSENUM_RE.search(name).end() + + synset_name, lemma_name = name[: separator - 1], name[separator:] + + synset = self.synset(synset_name) + for lemma in synset.lemmas(lang): + if lemma._name == lemma_name: + return lemma + raise WordNetError(f"No lemma {lemma_name!r} in {synset_name!r}") + + def lemma_from_key(self, key): + # Keys are case sensitive and always lower-case + key = key.lower() + + lemma_name, lex_sense = key.split("%") + pos_number, lexname_index, lex_id, _, _ = lex_sense.split(":") + pos = self._pos_names[int(pos_number)] + + # open the key -> synset file if necessary + if self._key_synset_file is None: + self._key_synset_file = self.open("index.sense") + + # Find the synset for the lemma. + synset_line = _binary_search_file(self._key_synset_file, key) + if not synset_line: + raise WordNetError("No synset found for key %r" % key) + offset = int(synset_line.split()[1]) + synset = self.synset_from_pos_and_offset(pos, offset) + # return the corresponding lemma + for lemma in synset._lemmas: + if lemma._key == key: + return lemma + raise WordNetError("No lemma found for for key %r" % key) + + ############################################################# + # Loading Synsets + ############################################################# + def synset(self, name): + # split name into lemma, part of speech and synset number + lemma, pos, synset_index_str = name.lower().rsplit(".", 2) + synset_index = int(synset_index_str) - 1 + + # get the offset for this synset + try: + offset = self._lemma_pos_offset_map[lemma][pos][synset_index] + except KeyError as e: + raise WordNetError(f"No lemma {lemma!r} with part of speech {pos!r}") from e + except IndexError as e: + n_senses = len(self._lemma_pos_offset_map[lemma][pos]) + raise WordNetError( + f"Lemma {lemma!r} with part of speech {pos!r} only " + f"has {n_senses} {'sense' if n_senses == 1 else 'senses'}" + ) from e + + # load synset information from the appropriate file + synset = self.synset_from_pos_and_offset(pos, offset) + + # some basic sanity checks on loaded attributes + if pos == "s" and synset._pos == "a": + message = ( + "Adjective satellite requested but only plain " + "adjective found for lemma %r" + ) + raise WordNetError(message % lemma) + assert synset._pos == pos or (pos == "a" and synset._pos == "s") + + # Return the synset object. + return synset + + def _data_file(self, pos): + """ + Return an open file pointer for the data file for the given + part of speech. + """ + if pos == ADJ_SAT: + pos = ADJ + if self._data_file_map.get(pos) is None: + fileid = "data.%s" % self._FILEMAP[pos] + self._data_file_map[pos] = self.open(fileid) + return self._data_file_map[pos] + + def synset_from_pos_and_offset(self, pos, offset): + """ + - pos: The synset's part of speech, matching one of the module level + attributes ADJ, ADJ_SAT, ADV, NOUN or VERB ('a', 's', 'r', 'n', or 'v'). + - offset: The byte offset of this synset in the WordNet dict file + for this pos. + + >>> from nltk.corpus import wordnet as wn + >>> print(wn.synset_from_pos_and_offset('n', 1740)) + Synset('entity.n.01') + """ + # Check to see if the synset is in the cache + if offset in self._synset_offset_cache[pos]: + return self._synset_offset_cache[pos][offset] + + data_file = self._data_file(pos) + data_file.seek(offset) + data_file_line = data_file.readline() + # If valid, the offset equals the 8-digit 0-padded integer found at the start of the line: + line_offset = data_file_line[:8] + if ( + line_offset.isalnum() + and line_offset == f"{'0'*(8-len(str(offset)))}{str(offset)}" + ): + synset = self._synset_from_pos_and_line(pos, data_file_line) + assert synset._offset == offset + self._synset_offset_cache[pos][offset] = synset + else: + synset = None + warnings.warn(f"No WordNet synset found for pos={pos} at offset={offset}.") + data_file.seek(0) + return synset + + @deprecated("Use public method synset_from_pos_and_offset() instead") + def _synset_from_pos_and_offset(self, *args, **kwargs): + """ + Hack to help people like the readers of + https://stackoverflow.com/a/27145655/1709587 + who were using this function before it was officially a public method + """ + return self.synset_from_pos_and_offset(*args, **kwargs) + + def _synset_from_pos_and_line(self, pos, data_file_line): + # Construct a new (empty) synset. + synset = Synset(self) + + # parse the entry for this synset + try: + # parse out the definitions and examples from the gloss + columns_str, gloss = data_file_line.strip().split("|") + definition = re.sub(r"[\"].*?[\"]", "", gloss).strip() + examples = re.findall(r'"([^"]*)"', gloss) + for example in examples: + synset._examples.append(example) + + synset._definition = definition.strip("; ") + + # split the other info into fields + _iter = iter(columns_str.split()) + + def _next_token(): + return next(_iter) + + # get the offset + synset._offset = int(_next_token()) + + # determine the lexicographer file name + lexname_index = int(_next_token()) + synset._lexname = self._lexnames[lexname_index] + + # get the part of speech + synset._pos = _next_token() + + # create Lemma objects for each lemma + n_lemmas = int(_next_token(), 16) + for _ in range(n_lemmas): + # get the lemma name + lemma_name = _next_token() + # get the lex_id (used for sense_keys) + lex_id = int(_next_token(), 16) + # If the lemma has a syntactic marker, extract it. + m = re.match(r"(.*?)(\(.*\))?$", lemma_name) + lemma_name, syn_mark = m.groups() + # create the lemma object + lemma = Lemma(self, synset, lemma_name, lexname_index, lex_id, syn_mark) + synset._lemmas.append(lemma) + synset._lemma_names.append(lemma._name) + + # collect the pointer tuples + n_pointers = int(_next_token()) + for _ in range(n_pointers): + symbol = _next_token() + offset = int(_next_token()) + pos = _next_token() + lemma_ids_str = _next_token() + if lemma_ids_str == "0000": + synset._pointers[symbol].add((pos, offset)) + else: + source_index = int(lemma_ids_str[:2], 16) - 1 + target_index = int(lemma_ids_str[2:], 16) - 1 + source_lemma_name = synset._lemmas[source_index]._name + lemma_pointers = synset._lemma_pointers + tups = lemma_pointers[source_lemma_name, symbol] + tups.append((pos, offset, target_index)) + + # read the verb frames + try: + frame_count = int(_next_token()) + except StopIteration: + pass + else: + for _ in range(frame_count): + # read the plus sign + plus = _next_token() + assert plus == "+" + # read the frame and lemma number + frame_number = int(_next_token()) + frame_string_fmt = VERB_FRAME_STRINGS[frame_number] + lemma_number = int(_next_token(), 16) + # lemma number of 00 means all words in the synset + if lemma_number == 0: + synset._frame_ids.append(frame_number) + for lemma in synset._lemmas: + lemma._frame_ids.append(frame_number) + lemma._frame_strings.append(frame_string_fmt % lemma._name) + # only a specific word in the synset + else: + lemma = synset._lemmas[lemma_number - 1] + lemma._frame_ids.append(frame_number) + lemma._frame_strings.append(frame_string_fmt % lemma._name) + + # raise a more informative error with line text + except ValueError as e: + raise WordNetError(f"line {data_file_line!r}: {e}") from e + + # set sense keys for Lemma objects - note that this has to be + # done afterwards so that the relations are available + for lemma in synset._lemmas: + if synset._pos == ADJ_SAT: + head_lemma = synset.similar_tos()[0]._lemmas[0] + head_name = head_lemma._name + head_id = "%02d" % head_lemma._lex_id + else: + head_name = head_id = "" + tup = ( + lemma._name, + WordNetCorpusReader._pos_numbers[synset._pos], + lemma._lexname_index, + lemma._lex_id, + head_name, + head_id, + ) + lemma._key = ("%s%%%d:%02d:%02d:%s:%s" % tup).lower() + + # the canonical name is based on the first lemma + lemma_name = synset._lemmas[0]._name.lower() + offsets = self._lemma_pos_offset_map[lemma_name][synset._pos] + sense_index = offsets.index(synset._offset) + tup = lemma_name, synset._pos, sense_index + 1 + synset._name = "%s.%s.%02i" % tup + + return synset + + def synset_from_sense_key(self, sense_key): + """ + Retrieves synset based on a given sense_key. Sense keys can be + obtained from lemma.key() + + From https://wordnet.princeton.edu/documentation/senseidx5wn: + A sense_key is represented as:: + + lemma % lex_sense (e.g. 'dog%1:18:01::') + + where lex_sense is encoded as:: + + ss_type:lex_filenum:lex_id:head_word:head_id + + :lemma: ASCII text of word/collocation, in lower case + :ss_type: synset type for the sense (1 digit int) + The synset type is encoded as follows:: + + 1 NOUN + 2 VERB + 3 ADJECTIVE + 4 ADVERB + 5 ADJECTIVE SATELLITE + :lex_filenum: name of lexicographer file containing the synset for the sense (2 digit int) + :lex_id: when paired with lemma, uniquely identifies a sense in the lexicographer file (2 digit int) + :head_word: lemma of the first word in satellite's head synset + Only used if sense is in an adjective satellite synset + :head_id: uniquely identifies sense in a lexicographer file when paired with head_word + Only used if head_word is present (2 digit int) + + >>> import nltk + >>> from nltk.corpus import wordnet as wn + >>> print(wn.synset_from_sense_key("drive%1:04:03::")) + Synset('drive.n.06') + + >>> print(wn.synset_from_sense_key("driving%1:04:03::")) + Synset('drive.n.06') + """ + return self.lemma_from_key(sense_key).synset() + + ############################################################# + # Retrieve synsets and lemmas. + ############################################################# + + def synsets(self, lemma, pos=None, lang="eng", check_exceptions=True): + """Load all synsets with a given lemma and part of speech tag. + If no pos is specified, all synsets for all parts of speech + will be loaded. + If lang is specified, all the synsets associated with the lemma name + of that language will be returned. + """ + lemma = lemma.lower() + + if lang == "eng": + get_synset = self.synset_from_pos_and_offset + index = self._lemma_pos_offset_map + if pos is None: + pos = POS_LIST + return [ + get_synset(p, offset) + for p in pos + for form in self._morphy(lemma, p, check_exceptions) + for offset in index[form].get(p, []) + ] + + else: + self._load_lang_data(lang) + synset_list = [] + if lemma in self._lang_data[lang][1]: + for l in self._lang_data[lang][1][lemma]: + if pos is not None and l[-1] != pos: + continue + synset_list.append(self.of2ss(l)) + return synset_list + + def lemmas(self, lemma, pos=None, lang="eng"): + """Return all Lemma objects with a name matching the specified lemma + name and part of speech tag. Matches any part of speech tag if none is + specified.""" + + lemma = lemma.lower() + if lang == "eng": + return [ + lemma_obj + for synset in self.synsets(lemma, pos) + for lemma_obj in synset.lemmas() + if lemma_obj.name().lower() == lemma + ] + + else: + self._load_lang_data(lang) + lemmas = [] + syn = self.synsets(lemma, lang=lang) + for s in syn: + if pos is not None and s.pos() != pos: + continue + for lemma_obj in s.lemmas(lang=lang): + if lemma_obj.name().lower() == lemma: + lemmas.append(lemma_obj) + return lemmas + + def all_lemma_names(self, pos=None, lang="eng"): + """Return all lemma names for all synsets for the given + part of speech tag and language or languages. If pos is + not specified, all synsets for all parts of speech will + be used.""" + + if lang == "eng": + if pos is None: + return iter(self._lemma_pos_offset_map) + else: + return ( + lemma + for lemma in self._lemma_pos_offset_map + if pos in self._lemma_pos_offset_map[lemma] + ) + else: + self._load_lang_data(lang) + lemma = [] + for i in self._lang_data[lang][0]: + if pos is not None and i[-1] != pos: + continue + lemma.extend(self._lang_data[lang][0][i]) + + lemma = iter(set(lemma)) + return lemma + + def all_omw_synsets(self, pos=None, lang=None): + if lang not in self.langs(): + return None + self._load_lang_data(lang) + for of in self._lang_data[lang][0]: + if not pos or of[-1] == pos: + ss = self.of2ss(of) + if ss: + yield ss + + # else: + # A few OMW offsets don't exist in Wordnet 3.0. + # warnings.warn(f"Language {lang}: no synset found for {of}") + + def all_synsets(self, pos=None, lang="eng"): + """Iterate over all synsets with a given part of speech tag. + If no pos is specified, all synsets for all parts of speech + will be loaded. + """ + if lang == "eng": + return self.all_eng_synsets(pos=pos) + else: + return self.all_omw_synsets(pos=pos, lang=lang) + + def all_eng_synsets(self, pos=None): + if pos is None: + pos_tags = self._FILEMAP.keys() + else: + pos_tags = [pos] + + cache = self._synset_offset_cache + from_pos_and_line = self._synset_from_pos_and_line + + # generate all synsets for each part of speech + for pos_tag in pos_tags: + # Open the file for reading. Note that we can not re-use + # the file pointers from self._data_file_map here, because + # we're defining an iterator, and those file pointers might + # be moved while we're not looking. + if pos_tag == ADJ_SAT: + pos_file = ADJ + else: + pos_file = pos_tag + fileid = "data.%s" % self._FILEMAP[pos_file] + data_file = self.open(fileid) + + try: + # generate synsets for each line in the POS file + offset = data_file.tell() + line = data_file.readline() + while line: + if not line[0].isspace(): + if offset in cache[pos_tag]: + # See if the synset is cached + synset = cache[pos_tag][offset] + else: + # Otherwise, parse the line + synset = from_pos_and_line(pos_tag, line) + cache[pos_tag][offset] = synset + + # adjective satellites are in the same file as + # adjectives so only yield the synset if it's actually + # a satellite + if pos_tag == ADJ_SAT and synset._pos == ADJ_SAT: + yield synset + # for all other POS tags, yield all synsets (this means + # that adjectives also include adjective satellites) + elif pos_tag != ADJ_SAT: + yield synset + offset = data_file.tell() + line = data_file.readline() + + # close the extra file handle we opened + except Exception: + data_file.close() + raise + else: + data_file.close() + + def words(self, lang="eng"): + """return lemmas of the given language as list of words""" + return self.all_lemma_names(lang=lang) + + def synonyms(self, word, lang="eng"): + """return nested list with the synonyms of the different senses of word in the given language""" + return [ + sorted(list(set(ss.lemma_names(lang=lang)) - {word})) + for ss in self.synsets(word, lang=lang) + ] + + def doc(self, file="README", lang="eng"): + """Return the contents of readme, license or citation file + use lang=lang to get the file for an individual language""" + if lang == "eng": + reader = self + else: + reader = self._omw_reader + if lang in self.langs(): + file = f"{os.path.join(self.provenances[lang],file)}" + try: + with reader.open(file) as fp: + return fp.read() + except Exception: + if lang in self._lang_data: + return f"Cannot determine {file} for {lang}" + else: + return f"Language {lang} is not supported." + + def license(self, lang="eng"): + """Return the contents of LICENSE (for omw) + use lang=lang to get the license for an individual language""" + return self.doc(file="LICENSE", lang=lang) + + def readme(self, lang="eng"): + """Return the contents of README (for omw) + use lang=lang to get the readme for an individual language""" + return self.doc(file="README", lang=lang) + + def citation(self, lang="eng"): + """Return the contents of citation.bib file (for omw) + use lang=lang to get the citation for an individual language""" + return self.doc(file="citation.bib", lang=lang) + + ############################################################# + # Misc + ############################################################# + def lemma_count(self, lemma): + """Return the frequency count for this Lemma""" + # Currently, count is only work for English + if lemma._lang != "eng": + return 0 + # open the count file if we haven't already + if self._key_count_file is None: + self._key_count_file = self.open("cntlist.rev") + # find the key in the counts file and return the count + line = _binary_search_file(self._key_count_file, lemma._key) + if line: + return int(line.rsplit(" ", 1)[-1]) + else: + return 0 + + def path_similarity(self, synset1, synset2, verbose=False, simulate_root=True): + return synset1.path_similarity(synset2, verbose, simulate_root) + + path_similarity.__doc__ = Synset.path_similarity.__doc__ + + def lch_similarity(self, synset1, synset2, verbose=False, simulate_root=True): + return synset1.lch_similarity(synset2, verbose, simulate_root) + + lch_similarity.__doc__ = Synset.lch_similarity.__doc__ + + def wup_similarity(self, synset1, synset2, verbose=False, simulate_root=True): + return synset1.wup_similarity(synset2, verbose, simulate_root) + + wup_similarity.__doc__ = Synset.wup_similarity.__doc__ + + def res_similarity(self, synset1, synset2, ic, verbose=False): + return synset1.res_similarity(synset2, ic, verbose) + + res_similarity.__doc__ = Synset.res_similarity.__doc__ + + def jcn_similarity(self, synset1, synset2, ic, verbose=False): + return synset1.jcn_similarity(synset2, ic, verbose) + + jcn_similarity.__doc__ = Synset.jcn_similarity.__doc__ + + def lin_similarity(self, synset1, synset2, ic, verbose=False): + return synset1.lin_similarity(synset2, ic, verbose) + + lin_similarity.__doc__ = Synset.lin_similarity.__doc__ + + ############################################################# + # Morphy + ############################################################# + # Morphy, adapted from Oliver Steele's pywordnet + def morphy(self, form, pos=None, check_exceptions=True): + """ + Find a possible base form for the given form, with the given + part of speech, by checking WordNet's list of exceptional + forms, or by substituting suffixes for this part of speech. + If pos=None, try every part of speech until finding lemmas. + Return the first form found in WordNet, or eventually None. + + >>> from nltk.corpus import wordnet as wn + >>> print(wn.morphy('dogs')) + dog + >>> print(wn.morphy('churches')) + church + >>> print(wn.morphy('aardwolves')) + aardwolf + >>> print(wn.morphy('abaci')) + abacus + >>> wn.morphy('hardrock', wn.ADV) + >>> print(wn.morphy('book', wn.NOUN)) + book + >>> wn.morphy('book', wn.ADJ) + """ + for pos in [pos] if pos else POS_LIST: + analyses = self._morphy(form, pos, check_exceptions) + if analyses: + # Stop (don't try more parts of speech): + return analyses[0] + + MORPHOLOGICAL_SUBSTITUTIONS = { + NOUN: [ + ("s", ""), + ("ses", "s"), + ("ves", "f"), + ("xes", "x"), + ("zes", "z"), + ("ches", "ch"), + ("shes", "sh"), + ("men", "man"), + ("ies", "y"), + ], + VERB: [ + ("s", ""), + ("ies", "y"), + ("es", "e"), + ("es", ""), + ("ed", "e"), + ("ed", ""), + ("ing", "e"), + ("ing", ""), + ], + ADJ: [("er", ""), ("est", ""), ("er", "e"), ("est", "e")], + ADV: [], + } + + MORPHOLOGICAL_SUBSTITUTIONS[ADJ_SAT] = MORPHOLOGICAL_SUBSTITUTIONS[ADJ] + + def _morphy(self, form, pos, check_exceptions=True): + # from jordanbg: + # Given an original string x + # 1. Apply rules once to the input to get y1, y2, y3, etc. + # 2. Return all that are in the database + # (edited by ekaf) If there are no matches return an empty list. + + exceptions = self._exception_map[pos] + substitutions = self.MORPHOLOGICAL_SUBSTITUTIONS[pos] + + def apply_rules(forms): + return [ + form[: -len(old)] + new + for form in forms + for old, new in substitutions + if form.endswith(old) + ] + + def filter_forms(forms): + result = [] + seen = set() + for form in forms: + if form in self._lemma_pos_offset_map: + if pos in self._lemma_pos_offset_map[form]: + if form not in seen: + result.append(form) + seen.add(form) + return result + + if check_exceptions and form in exceptions: + # 0. Check the exception lists + forms = exceptions[form] + else: + # 1. Apply rules once to the input to get y1, y2, y3, etc. + forms = apply_rules([form]) + + # 2. Return all that are in the database (and check the original too) + return filter_forms([form] + forms) + + def tag2pos(self, tag, tagset="en-ptb"): + """ + Convert a tag from one of the tagsets in nltk_data/taggers/universal_tagset to a + WordNet Part-of-Speech, using Universal Tags (Petrov et al., 2012) as intermediary. + Return None when WordNet does not cover that POS. + + :param tag: The part-of-speech tag to convert. + :type tag: str + :param tagset: The tagset of the input tag. Defaults to "en-ptb". + Supported tagsets are those recognized by the `map_tag` function + from `nltk.tag`. Common examples include: + - "en-ptb" (Penn Treebank tagset for English) + - "en-brown" (Brown tagset) + For a complete list of supported tagsets, refer to the `map_tag` + documentation or its source code in the NLTK library. + :type tagset: str + + :returns: The corresponding WordNet POS tag ('n', 'v', 'a', 'r') or None + if the tag cannot be mapped to a WordNet POS. + :rtype: str or None + + Example: + >>> import nltk + >>> tagged = nltk.tag.pos_tag(nltk.tokenize.word_tokenize("Banks check books.")) + >>> print([(word, tag, nltk.corpus.wordnet.tag2pos(tag)) for word, tag in tagged]) + [('Banks', 'NNS', 'n'), ('check', 'VBP', 'v'), ('books', 'NNS', 'n'), ('.', '.', None)] + """ + if tagset != "universal": + tag = map_tag(tagset, "universal", tag) + + return UNIVERSAL_TAG_TO_WN_POS.get(tag, None) + + ############################################################# + # Create information content from corpus + ############################################################# + def ic(self, corpus, weight_senses_equally=False, smoothing=1.0): + """ + Creates an information content lookup dictionary from a corpus. + + :type corpus: CorpusReader + :param corpus: The corpus from which we create an information + content dictionary. + :type weight_senses_equally: bool + :param weight_senses_equally: If this is True, gives all + possible senses equal weight rather than dividing by the + number of possible senses. (If a word has 3 synses, each + sense gets 0.3333 per appearance when this is False, 1.0 when + it is true.) + :param smoothing: How much do we smooth synset counts (default is 1.0) + :type smoothing: float + :return: An information content dictionary + """ + counts = FreqDist() + for ww in corpus.words(): + counts[ww] += 1 + + ic = {} + for pp in POS_LIST: + ic[pp] = defaultdict(float) + + # Initialize the counts with the smoothing value + if smoothing > 0.0: + for pp in POS_LIST: + ic[pp][0] = smoothing + for ss in self.all_synsets(): + pos = ss._pos + if pos == ADJ_SAT: + pos = ADJ + ic[pos][ss._offset] = smoothing + + for ww in counts: + possible_synsets = self.synsets(ww) + if len(possible_synsets) == 0: + continue + + # Distribute weight among possible synsets + weight = float(counts[ww]) + if not weight_senses_equally: + weight /= float(len(possible_synsets)) + + for ss in possible_synsets: + pos = ss._pos + if pos == ADJ_SAT: + pos = ADJ + for level in ss._iter_hypernym_lists(): + for hh in level: + ic[pos][hh._offset] += weight + # Add the weight to the root + ic[pos][0] += weight + return ic + + def custom_lemmas(self, tab_file, lang): + """ + Reads a custom tab file containing mappings of lemmas in the given + language to Princeton WordNet 3.0 synset offsets, allowing NLTK's + WordNet functions to then be used with that language. + + See the "Tab files" section at https://omwn.org/omw1.html for + documentation on the Multilingual WordNet tab file format. + + :param tab_file: Tab file as a file or file-like object + :type: lang str + :param: lang ISO 639-3 code of the language of the tab file + """ + lg = lang.split("_")[0] + if len(lg) != 3: + raise ValueError("lang should be a (3 character) ISO 639-3 code") + self._lang_data[lang] = [ + defaultdict(list), + defaultdict(list), + defaultdict(list), + defaultdict(list), + ] + for line in tab_file.readlines(): + if isinstance(line, bytes): + # Support byte-stream files (e.g. as returned by Python 2's + # open() function) as well as text-stream ones + line = line.decode("utf-8") + if not line.startswith("#"): + triple = line.strip().split("\t") + if len(triple) < 3: + continue + offset_pos, label = triple[:2] + val = triple[-1] + if self.map30: + if offset_pos in self.map30: + # Map offset_pos to current Wordnet version: + offset_pos = self.map30[offset_pos] + else: + # Some OMW offsets were never in Wordnet: + if ( + offset_pos not in self.nomap["wordnet"] + and offset_pos.replace("a", "s") + not in self.nomap["wordnet"] + ): + warnings.warn( + f"{lang}: invalid offset {offset_pos} in '{line}'" + ) + continue + elif offset_pos[-1] == "a": + wnss = self.of2ss(offset_pos) + if wnss and wnss.pos() == "s": # Wordnet pos is "s" + # Label OMW adjective satellites back to their Wordnet pos ("s") + offset_pos = self.ss2of(wnss) + pair = label.split(":") + attr = pair[-1] + if len(pair) == 1 or pair[0] == lg: + if attr == "lemma": + val = val.strip().replace(" ", "_") + lang_offsets = self._lang_data[lang][1][val.lower()] + if offset_pos not in lang_offsets: + lang_offsets.append(offset_pos) + if attr in self.lg_attrs: + lang_lemmas = self._lang_data[lang][self.lg_attrs.index(attr)][ + offset_pos + ] + if val not in lang_lemmas: + lang_lemmas.append(val) + + def disable_custom_lemmas(self, lang): + """prevent synsets from being mistakenly added""" + for n in range(len(self.lg_attrs)): + self._lang_data[lang][n].default_factory = None + + ###################################################################### + # Visualize WordNet relation graphs using Graphviz + ###################################################################### + + def digraph( + self, + inputs, + rel=lambda s: s.hypernyms(), + pos=None, + maxdepth=-1, + shapes=None, + attr=None, + verbose=False, + ): + """ + Produce a graphical representation from 'inputs' (a list of + start nodes, which can be a mix of Synsets, Lemmas and/or words), + and a synset relation, for drawing with the 'dot' graph visualisation + program from the Graphviz package. + + Return a string in the DOT graph file language, which can then be + converted to an image by nltk.parse.dependencygraph.dot2img(dot_string). + + Optional Parameters: + :rel: Wordnet synset relation + :pos: for words, restricts Part of Speech to 'n', 'v', 'a' or 'r' + :maxdepth: limit the longest path + :shapes: dictionary of strings that trigger a specified shape + :attr: dictionary with global graph attributes + :verbose: warn about cycles + + >>> from nltk.corpus import wordnet as wn + >>> print(wn.digraph([wn.synset('dog.n.01')])) + digraph G { + "Synset('animal.n.01')" -> "Synset('organism.n.01')"; + "Synset('canine.n.02')" -> "Synset('carnivore.n.01')"; + "Synset('carnivore.n.01')" -> "Synset('placental.n.01')"; + "Synset('chordate.n.01')" -> "Synset('animal.n.01')"; + "Synset('dog.n.01')" -> "Synset('canine.n.02')"; + "Synset('dog.n.01')" -> "Synset('domestic_animal.n.01')"; + "Synset('domestic_animal.n.01')" -> "Synset('animal.n.01')"; + "Synset('living_thing.n.01')" -> "Synset('whole.n.02')"; + "Synset('mammal.n.01')" -> "Synset('vertebrate.n.01')"; + "Synset('object.n.01')" -> "Synset('physical_entity.n.01')"; + "Synset('organism.n.01')" -> "Synset('living_thing.n.01')"; + "Synset('physical_entity.n.01')" -> "Synset('entity.n.01')"; + "Synset('placental.n.01')" -> "Synset('mammal.n.01')"; + "Synset('vertebrate.n.01')" -> "Synset('chordate.n.01')"; + "Synset('whole.n.02')" -> "Synset('object.n.01')"; + } + + """ + from nltk.util import edge_closure, edges2dot + + synsets = set() + edges = set() + if not shapes: + shapes = dict() + if not attr: + attr = dict() + + def add_lemma(lem): + ss = lem.synset() + synsets.add(ss) + edges.add((lem, ss)) + + for node in inputs: + typ = type(node) + if typ == Synset: + synsets.add(node) + elif typ == Lemma: + add_lemma(node) + elif typ == str: + for lemma in self.lemmas(node, pos): + add_lemma(lemma) + + for ss in synsets: + edges = edges.union(edge_closure(ss, rel, maxdepth, verbose)) + dot_string = edges2dot(sorted(list(edges)), shapes=shapes, attr=attr) + return dot_string + + +###################################################################### +# WordNet Information Content Corpus Reader +###################################################################### + + +class WordNetICCorpusReader(CorpusReader): + """ + A corpus reader for the WordNet information content corpus. + """ + + def __init__(self, root, fileids): + CorpusReader.__init__(self, root, fileids, encoding="utf8") + + # this load function would be more efficient if the data was pickled + # Note that we can't use NLTK's frequency distributions because + # synsets are overlapping (each instance of a synset also counts + # as an instance of its hypernyms) + def ic(self, icfile): + """ + Load an information content file from the wordnet_ic corpus + and return a dictionary. This dictionary has just two keys, + NOUN and VERB, whose values are dictionaries that map from + synsets to information content values. + + :type icfile: str + :param icfile: The name of the wordnet_ic file (e.g. "ic-brown.dat") + :return: An information content dictionary + """ + ic = {} + ic[NOUN] = defaultdict(float) + ic[VERB] = defaultdict(float) + with self.open(icfile) as fp: + for num, line in enumerate(fp): + if num == 0: # skip the header + continue + fields = line.split() + offset = int(fields[0][:-1]) + value = float(fields[1]) + pos = _get_pos(fields[0]) + if len(fields) == 3 and fields[2] == "ROOT": + # Store root count. + ic[pos][0] += value + if value != 0: + ic[pos][offset] = value + return ic + + +###################################################################### +# Similarity metrics +###################################################################### + +# TODO: Add in the option to manually add a new root node; this will be +# useful for verb similarity as there exist multiple verb taxonomies. + +# More information about the metrics is available at +# http://marimba.d.umn.edu/similarity/measures.html + + +def path_similarity(synset1, synset2, verbose=False, simulate_root=True): + return synset1.path_similarity( + synset2, verbose=verbose, simulate_root=simulate_root + ) + + +def lch_similarity(synset1, synset2, verbose=False, simulate_root=True): + return synset1.lch_similarity(synset2, verbose=verbose, simulate_root=simulate_root) + + +def wup_similarity(synset1, synset2, verbose=False, simulate_root=True): + return synset1.wup_similarity(synset2, verbose=verbose, simulate_root=simulate_root) + + +def res_similarity(synset1, synset2, ic, verbose=False): + return synset1.res_similarity(synset2, ic, verbose=verbose) + + +def jcn_similarity(synset1, synset2, ic, verbose=False): + return synset1.jcn_similarity(synset2, ic, verbose=verbose) + + +def lin_similarity(synset1, synset2, ic, verbose=False): + return synset1.lin_similarity(synset2, ic, verbose=verbose) + + +path_similarity.__doc__ = Synset.path_similarity.__doc__ +lch_similarity.__doc__ = Synset.lch_similarity.__doc__ +wup_similarity.__doc__ = Synset.wup_similarity.__doc__ +res_similarity.__doc__ = Synset.res_similarity.__doc__ +jcn_similarity.__doc__ = Synset.jcn_similarity.__doc__ +lin_similarity.__doc__ = Synset.lin_similarity.__doc__ + + +def _lcs_ic(synset1, synset2, ic, verbose=False): + """ + Get the information content of the least common subsumer that has + the highest information content value. If two nodes have no + explicit common subsumer, assume that they share an artificial + root node that is the hypernym of all explicit roots. + + :type synset1: Synset + :param synset1: First input synset. + :type synset2: Synset + :param synset2: Second input synset. Must be the same part of + speech as the first synset. + :type ic: dict + :param ic: an information content object (as returned by ``load_ic()``). + :return: The information content of the two synsets and their most + informative subsumer + """ + if synset1._pos != synset2._pos: + raise WordNetError( + "Computing the least common subsumer requires " + "%s and %s to have the same part of speech." % (synset1, synset2) + ) + + ic1 = information_content(synset1, ic) + ic2 = information_content(synset2, ic) + subsumers = synset1.common_hypernyms(synset2) + if len(subsumers) == 0: + subsumer_ic = 0 + else: + subsumer_ic = max(information_content(s, ic) for s in subsumers) + + if verbose: + print("> LCS Subsumer by content:", subsumer_ic) + + return ic1, ic2, subsumer_ic + + +# Utility functions + + +def information_content(synset, ic): + pos = synset._pos + if pos == ADJ_SAT: + pos = ADJ + try: + icpos = ic[pos] + except KeyError as e: + msg = "Information content file has no entries for part-of-speech: %s" + raise WordNetError(msg % pos) from e + + counts = icpos[synset._offset] + if counts == 0: + return _INF + else: + return -math.log(counts / icpos[0]) + + +# get the part of speech (NOUN or VERB) from the information content record +# (each identifier has a 'n' or 'v' suffix) + + +def _get_pos(field): + if field[-1] == "n": + return NOUN + elif field[-1] == "v": + return VERB + else: + msg = ( + "Unidentified part of speech in WordNet Information Content file " + "for field %s" % field + ) + raise ValueError(msg) diff --git a/nltk/corpus/reader/xmldocs.py b/nltk/corpus/reader/xmldocs.py new file mode 100644 index 0000000..58d810a --- /dev/null +++ b/nltk/corpus/reader/xmldocs.py @@ -0,0 +1,417 @@ +# Natural Language Toolkit: XML Corpus Reader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Corpus reader for corpora whose documents are xml files. + +(note -- not named 'xml' to avoid conflicting w/ standard xml package) +""" + +import codecs + +# Parse untrusted corpus XML with defusedxml, which forbids the custom-entity +# definitions used by XML entity-expansion (Billion Laughs, CWE-776) attacks +# while leaving ordinary XML (including the standard & < ... entities) +# unaffected. See issue #3545 / PR #3544, which applied the same guard to the +# downloader's remote index. +from defusedxml.ElementTree import fromstring as safe_fromstring +from defusedxml.ElementTree import parse as safe_parse + +from nltk.corpus.reader.api import CorpusReader +from nltk.corpus.reader.util import * +from nltk.data import SeekableUnicodeStreamReader +from nltk.internals import ElementWrapper +from nltk.tokenize import WordPunctTokenizer + + +class XMLCorpusReader(CorpusReader): + """ + Corpus reader for corpora whose documents are xml files. + + Note that the ``XMLCorpusReader`` constructor does not take an + ``encoding`` argument, because the unicode encoding is specified by + the XML files themselves. See the XML specs for more info. + """ + + def __init__(self, root, fileids, wrap_etree=False): + self._wrap_etree = wrap_etree + CorpusReader.__init__(self, root, fileids) + + def xml(self, fileid=None): + # Make sure we have exactly one file -- no concatenating XML. + if fileid is None and len(self._fileids) == 1: + fileid = self._fileids[0] + if not isinstance(fileid, str): + raise TypeError("Expected a single file identifier string") + # Read the XML in using defusedxml's ElementTree. + with self.abspath(fileid).open() as fp: + elt = safe_parse(fp).getroot() + # If requested, wrap it. + if self._wrap_etree: + elt = ElementWrapper(elt) + # Return the ElementTree element. + return elt + + def words(self, fileid=None): + """ + Returns all of the words and punctuation symbols in the specified file + that were in text nodes -- ie, tags are ignored. Like the xml() method, + fileid can only specify one file. + + :return: the given file's text nodes as a list of words and punctuation symbols + :rtype: list(str) + """ + + elt = self.xml(fileid) + encoding = self.encoding(fileid) + word_tokenizer = WordPunctTokenizer() + try: + iterator = elt.getiterator() + except AttributeError: + iterator = elt.iter() + out = [] + + for node in iterator: + text = node.text + if text is not None: + if isinstance(text, bytes): + text = text.decode(encoding) + toks = word_tokenizer.tokenize(text) + out.extend(toks) + return out + + +class XMLCorpusView(StreamBackedCorpusView): + """ + A corpus view that selects out specified elements from an XML + file, and provides a flat list-like interface for accessing them. + (Note: ``XMLCorpusView`` is not used by ``XMLCorpusReader`` itself, + but may be used by subclasses of ``XMLCorpusReader``.) + + Every XML corpus view has a "tag specification", indicating what + XML elements should be included in the view; and each (non-nested) + element that matches this specification corresponds to one item in + the view. Tag specifications are regular expressions over tag + paths, where a tag path is a list of element tag names, separated + by '/', indicating the ancestry of the element. Some examples: + + - ``'foo'``: A top-level element whose tag is ``foo``. + - ``'foo/bar'``: An element whose tag is ``bar`` and whose parent + is a top-level element whose tag is ``foo``. + - ``'.*/foo'``: An element whose tag is ``foo``, appearing anywhere + in the xml tree. + - ``'.*/(foo|bar)'``: An wlement whose tag is ``foo`` or ``bar``, + appearing anywhere in the xml tree. + + The view items are generated from the selected XML elements via + the method ``handle_elt()``. By default, this method returns the + element as-is (i.e., as an ElementTree object); but it can be + overridden, either via subclassing or via the ``elt_handler`` + constructor parameter. + """ + + #: If true, then display debugging output to stdout when reading + #: blocks. + _DEBUG = False + + #: The number of characters read at a time by this corpus reader. + _BLOCK_SIZE = 1024 + + def __init__(self, fileid, tagspec, elt_handler=None): + """ + Create a new corpus view based on a specified XML file. + + Note that the ``XMLCorpusView`` constructor does not take an + ``encoding`` argument, because the unicode encoding is + specified by the XML files themselves. + + :type tagspec: str + :param tagspec: A tag specification, indicating what XML + elements should be included in the view. Each non-nested + element that matches this specification corresponds to one + item in the view. + + :param elt_handler: A function used to transform each element + to a value for the view. If no handler is specified, then + ``self.handle_elt()`` is called, which returns the element + as an ElementTree object. The signature of elt_handler is:: + + elt_handler(elt, tagspec) -> value + """ + if elt_handler: + self.handle_elt = elt_handler + + self._tagspec = re.compile(tagspec + r"\Z") + """The tag specification for this corpus view.""" + + self._tag_context = {0: ()} + """A dictionary mapping from file positions (as returned by + ``stream.seek()`` to XML contexts. An XML context is a + tuple of XML tag names, indicating which tags have not yet + been closed.""" + + encoding = self._detect_encoding(fileid) + StreamBackedCorpusView.__init__(self, fileid, encoding=encoding) + + def _detect_encoding(self, fileid): + if isinstance(fileid, PathPointer): + try: + infile = fileid.open() + s = infile.readline() + finally: + infile.close() + else: + with open(fileid, "rb") as infile: + s = infile.readline() + if s.startswith(codecs.BOM_UTF16_BE): + return "utf-16-be" + if s.startswith(codecs.BOM_UTF16_LE): + return "utf-16-le" + if s.startswith(codecs.BOM_UTF32_BE): + return "utf-32-be" + if s.startswith(codecs.BOM_UTF32_LE): + return "utf-32-le" + if s.startswith(codecs.BOM_UTF8): + return "utf-8" + m = re.match(rb'\s*<\?xml\b.*\bencoding="([^"]+)"', s) + if m: + return m.group(1).decode() + m = re.match(rb"\s*<\?xml\b.*\bencoding='([^']+)'", s) + if m: + return m.group(1).decode() + # No encoding found -- what should the default be? + return "utf-8" + + def handle_elt(self, elt, context): + """ + Convert an element into an appropriate value for inclusion in + the view. Unless overridden by a subclass or by the + ``elt_handler`` constructor argument, this method simply + returns ``elt``. + + :return: The view value corresponding to ``elt``. + + :type elt: ElementTree + :param elt: The element that should be converted. + + :type context: str + :param context: A string composed of element tags separated by + forward slashes, indicating the XML context of the given + element. For example, the string ``'foo/bar/baz'`` + indicates that the element is a ``baz`` element whose + parent is a ``bar`` element and whose grandparent is a + top-level ``foo`` element. + """ + return elt + + #: A regular expression that matches XML fragments that do not + #: contain any un-closed tags. + # + # Each delimited alternative is pinned to its own terminator so it cannot + # span across it: the comment body is "any run that does not start ``-->``" + # and the CDATA body "any run that does not start ``]]>``", rather than a + # lazy ``.*?`` which, with re.DOTALL, matches across the terminator and so + # spans several comments/sections. The lazy form makes the repeated group + # ambiguous: when the final ``\Z`` fails (e.g. the fragment ends with an + # unterminated comment) the engine tries exponentially many ways to + # partition the input -- a catastrophic-backtracking ReDoS (CWE-1333). + # Likewise the doctype's pre-subset run excludes ``>``. Pinning each piece + # to its first terminator keeps validation linear while matching the same + # well-formed fragments. (The CDATA brackets are also escaped so they match + # a literal ``).)*-->) | # comment + ().)*\]\]>) | # raw character data + (]*(\[[^\]]*])?\s*>) | # doctype decl + (<[^!>][^>]*>)) # tag or PI + [^<]*)* + \Z""", + re.DOTALL | re.VERBOSE, + ) + + #: A regular expression used to extract the tag name from a start tag, + #: end tag, or empty-elt tag string. + _XML_TAG_NAME = re.compile(r"<\s*(?:/\s*)?([^\s>]+)") + + #: A regular expression used to find all start-tags, end-tags, and + #: empty-elt tags in an XML file. This regexp is more lenient than + #: the XML spec -- e.g., it allows spaces in some places where the + #: spec does not. + _XML_PIECE = re.compile( + r""" + # Include these so we can skip them: + (?P )| + (?P )| + (?P <\?.*?\?> )| + (?P ]*(\[[^\]]*])?\s*>)| + # These are the ones we actually care about: + (?P <\s*[^>/\?!\s][^>]*/\s*> )| + (?P <\s*[^>/\?!\s][^>]*> )| + (?P <\s*/[^>/\?!\s][^>]*> )""", + re.DOTALL | re.VERBOSE, + ) + + def _read_xml_fragment(self, stream): + """ + Read a string from the given stream that does not contain any + un-closed tags. In particular, this function first reads a + block from the stream of size ``self._BLOCK_SIZE``. It then + checks if that block contains an un-closed tag. If it does, + then this function either backtracks to the last '<', or reads + another block. + """ + fragment = "" + + if isinstance(stream, SeekableUnicodeStreamReader): + startpos = stream.tell() + while True: + # Read a block and add it to the fragment. + xml_block = stream.read(self._BLOCK_SIZE) + fragment += xml_block + + # Do we have a well-formed xml fragment? + if self._VALID_XML_RE.match(fragment): + return fragment + + # Do we have a fragment that will never be well-formed? + if re.search("[<>]", fragment).group(0) == ">": + pos = stream.tell() - ( + len(fragment) - re.search("[<>]", fragment).end() + ) + raise ValueError('Unexpected ">" near char %s' % pos) + + # End of file? + if not xml_block: + raise ValueError("Unexpected end of file: tag not closed") + + # If not, then we must be in the middle of a <..tag..>. + # If appropriate, backtrack to the most recent '<' + # character. + last_open_bracket = fragment.rfind("<") + if last_open_bracket > 0: + if self._VALID_XML_RE.match(fragment[:last_open_bracket]): + if isinstance(stream, SeekableUnicodeStreamReader): + stream.seek(startpos) + stream.char_seek_forward(last_open_bracket) + else: + stream.seek(-(len(fragment) - last_open_bracket), 1) + return fragment[:last_open_bracket] + + # Otherwise, read another block. (i.e., return to the + # top of the loop.) + + def read_block(self, stream, tagspec=None, elt_handler=None): + """ + Read from ``stream`` until we find at least one element that + matches ``tagspec``, and return the result of applying + ``elt_handler`` to each element found. + """ + if tagspec is None: + tagspec = self._tagspec + if elt_handler is None: + elt_handler = self.handle_elt + + # Use a stack of strings to keep track of our context: + context = list(self._tag_context.get(stream.tell())) + assert context is not None # check this -- could it ever happen? + + elts = [] + + elt_start = None # where does the elt start + elt_depth = None # what context depth + elt_text = "" + + while elts == [] or elt_start is not None: + if isinstance(stream, SeekableUnicodeStreamReader): + startpos = stream.tell() + xml_fragment = self._read_xml_fragment(stream) + + # End of file. + if not xml_fragment: + if elt_start is None: + break + else: + raise ValueError("Unexpected end of file") + + # Process each in the xml fragment. + for piece in self._XML_PIECE.finditer(xml_fragment): + if self._DEBUG: + print("{:>25} {}".format("/".join(context)[-20:], piece.group())) + + if piece.group("START_TAG"): + name = self._XML_TAG_NAME.match(piece.group()).group(1) + # Keep context up-to-date. + context.append(name) + # Is this one of the elts we're looking for? + if elt_start is None: + if re.match(tagspec, "/".join(context)): + elt_start = piece.start() + elt_depth = len(context) + + elif piece.group("END_TAG"): + name = self._XML_TAG_NAME.match(piece.group()).group(1) + # sanity checks: + if not context: + raise ValueError("Unmatched tag " % name) + if name != context[-1]: + raise ValueError(f"Unmatched tag <{context[-1]}>...") + # Is this the end of an element? + if elt_start is not None and elt_depth == len(context): + elt_text += xml_fragment[elt_start : piece.end()] + elts.append((elt_text, "/".join(context))) + elt_start = elt_depth = None + elt_text = "" + # Keep context up-to-date + context.pop() + + elif piece.group("EMPTY_ELT_TAG"): + name = self._XML_TAG_NAME.match(piece.group()).group(1) + if elt_start is None: + if re.match(tagspec, "/".join(context) + "/" + name): + elts.append((piece.group(), "/".join(context) + "/" + name)) + + if elt_start is not None: + # If we haven't found any elements yet, then keep + # looping until we do. + if elts == []: + elt_text += xml_fragment[elt_start:] + elt_start = 0 + + # If we've found at least one element, then try + # backtracking to the start of the element that we're + # inside of. + else: + # take back the last start-tag, and return what + # we've gotten so far (elts is non-empty). + if self._DEBUG: + print(" " * 36 + "(backtrack)") + if isinstance(stream, SeekableUnicodeStreamReader): + stream.seek(startpos) + stream.char_seek_forward(elt_start) + else: + stream.seek(-(len(xml_fragment) - elt_start), 1) + context = context[: elt_depth - 1] + elt_start = elt_depth = None + elt_text = "" + + # Update the _tag_context dict. + pos = stream.tell() + if pos in self._tag_context: + assert tuple(context) == self._tag_context[pos] + else: + self._tag_context[pos] = tuple(context) + + return [ + elt_handler( + safe_fromstring(elt.encode("ascii", "xmlcharrefreplace")), + context, + ) + for (elt, context) in elts + ] diff --git a/nltk/corpus/reader/ycoe.py b/nltk/corpus/reader/ycoe.py new file mode 100644 index 0000000..db39538 --- /dev/null +++ b/nltk/corpus/reader/ycoe.py @@ -0,0 +1,256 @@ +# Natural Language Toolkit: York-Toronto-Helsinki Parsed Corpus of Old English Prose (YCOE) +# +# Copyright (C) 2001-2015 NLTK Project +# Author: Selina Dennis +# URL: +# For license information, see LICENSE.TXT + +""" +Corpus reader for the York-Toronto-Helsinki Parsed Corpus of Old +English Prose (YCOE), a 1.5 million word syntactically-annotated +corpus of Old English prose texts. The corpus is distributed by the +Oxford Text Archive: http://www.ota.ahds.ac.uk/ It is not included +with NLTK. + +The YCOE corpus is divided into 100 files, each representing +an Old English prose text. Tags used within each text complies +to the YCOE standard: https://www-users.york.ac.uk/~lang22/YCOE/YcoeHome.htm +""" + +import os +import re + +from nltk.corpus.reader.api import * +from nltk.corpus.reader.bracket_parse import BracketParseCorpusReader +from nltk.corpus.reader.tagged import TaggedCorpusReader +from nltk.corpus.reader.util import * +from nltk.tokenize import RegexpTokenizer + + +class YCOECorpusReader(CorpusReader): + """ + Corpus reader for the York-Toronto-Helsinki Parsed Corpus of Old + English Prose (YCOE), a 1.5 million word syntactically-annotated + corpus of Old English prose texts. + """ + + def __init__(self, root, encoding="utf8"): + CorpusReader.__init__(self, root, [], encoding) + + self._psd_reader = YCOEParseCorpusReader( + self.root.join("psd"), ".*", ".psd", encoding=encoding + ) + self._pos_reader = YCOETaggedCorpusReader(self.root.join("pos"), ".*", ".pos") + + # Make sure we have a consistent set of items: + documents = {f[:-4] for f in self._psd_reader.fileids()} + if {f[:-4] for f in self._pos_reader.fileids()} != documents: + raise ValueError('Items in "psd" and "pos" ' "subdirectories do not match.") + + fileids = sorted( + ["%s.psd" % doc for doc in documents] + + ["%s.pos" % doc for doc in documents] + ) + CorpusReader.__init__(self, root, fileids, encoding) + self._documents = sorted(documents) + + def documents(self, fileids=None): + """ + Return a list of document identifiers for all documents in + this corpus, or for the documents with the given file(s) if + specified. + """ + if fileids is None: + return self._documents + if isinstance(fileids, str): + fileids = [fileids] + for f in fileids: + if f not in self._fileids: + raise KeyError("File id %s not found" % fileids) + # Strip off the '.pos' and '.psd' extensions. + return sorted({f[:-4] for f in fileids}) + + def fileids(self, documents=None): + """ + Return a list of file identifiers for the files that make up + this corpus, or that store the given document(s) if specified. + """ + if documents is None: + return self._fileids + elif isinstance(documents, str): + documents = [documents] + return sorted( + set( + ["%s.pos" % doc for doc in documents] + + ["%s.psd" % doc for doc in documents] + ) + ) + + def _getfileids(self, documents, subcorpus): + """ + Helper that selects the appropriate fileids for a given set of + documents from a given subcorpus (pos or psd). + """ + if documents is None: + documents = self._documents + else: + if isinstance(documents, str): + documents = [documents] + for document in documents: + if document not in self._documents: + if document[-4:] in (".pos", ".psd"): + raise ValueError( + "Expected a document identifier, not a file " + "identifier. (Use corpus.documents() to get " + "a list of document identifiers." + ) + else: + raise ValueError("Document identifier %s not found" % document) + return [f"{d}.{subcorpus}" for d in documents] + + # Delegate to one of our two sub-readers: + def words(self, documents=None): + return self._pos_reader.words(self._getfileids(documents, "pos")) + + def sents(self, documents=None): + return self._pos_reader.sents(self._getfileids(documents, "pos")) + + def paras(self, documents=None): + return self._pos_reader.paras(self._getfileids(documents, "pos")) + + def tagged_words(self, documents=None): + return self._pos_reader.tagged_words(self._getfileids(documents, "pos")) + + def tagged_sents(self, documents=None): + return self._pos_reader.tagged_sents(self._getfileids(documents, "pos")) + + def tagged_paras(self, documents=None): + return self._pos_reader.tagged_paras(self._getfileids(documents, "pos")) + + def parsed_sents(self, documents=None): + return self._psd_reader.parsed_sents(self._getfileids(documents, "psd")) + + +class YCOEParseCorpusReader(BracketParseCorpusReader): + """Specialized version of the standard bracket parse corpus reader + that strips out (CODE ...) and (ID ...) nodes.""" + + def _parse(self, t): + t = re.sub(r"(?u)\((CODE|ID)[^\)]*\)", "", t) + if re.match(r"\s*\(\s*\)\s*$", t): + return None + return BracketParseCorpusReader._parse(self, t) + + +class YCOETaggedCorpusReader(TaggedCorpusReader): + def __init__(self, root, items, encoding="utf8"): + gaps_re = r"(?u)(?<=/\.)\s+|\s*\S*_CODE\s*|\s*\S*_ID\s*" + sent_tokenizer = RegexpTokenizer(gaps_re, gaps=True) + TaggedCorpusReader.__init__( + self, root, items, sep="_", sent_tokenizer=sent_tokenizer + ) + + +#: A list of all documents and their titles in ycoe. +documents = { + "coadrian.o34": "Adrian and Ritheus", + "coaelhom.o3": "Ælfric, Supplemental Homilies", + "coaelive.o3": "Ælfric's Lives of Saints", + "coalcuin": "Alcuin De virtutibus et vitiis", + "coalex.o23": "Alexander's Letter to Aristotle", + "coapollo.o3": "Apollonius of Tyre", + "coaugust": "Augustine", + "cobede.o2": "Bede's History of the English Church", + "cobenrul.o3": "Benedictine Rule", + "coblick.o23": "Blickling Homilies", + "coboeth.o2": "Boethius' Consolation of Philosophy", + "cobyrhtf.o3": "Byrhtferth's Manual", + "cocanedgD": "Canons of Edgar (D)", + "cocanedgX": "Canons of Edgar (X)", + "cocathom1.o3": "Ælfric's Catholic Homilies I", + "cocathom2.o3": "Ælfric's Catholic Homilies II", + "cochad.o24": "Saint Chad", + "cochdrul": "Chrodegang of Metz, Rule", + "cochristoph": "Saint Christopher", + "cochronA.o23": "Anglo-Saxon Chronicle A", + "cochronC": "Anglo-Saxon Chronicle C", + "cochronD": "Anglo-Saxon Chronicle D", + "cochronE.o34": "Anglo-Saxon Chronicle E", + "cocura.o2": "Cura Pastoralis", + "cocuraC": "Cura Pastoralis (Cotton)", + "codicts.o34": "Dicts of Cato", + "codocu1.o1": "Documents 1 (O1)", + "codocu2.o12": "Documents 2 (O1/O2)", + "codocu2.o2": "Documents 2 (O2)", + "codocu3.o23": "Documents 3 (O2/O3)", + "codocu3.o3": "Documents 3 (O3)", + "codocu4.o24": "Documents 4 (O2/O4)", + "coeluc1": "Honorius of Autun, Elucidarium 1", + "coeluc2": "Honorius of Autun, Elucidarium 1", + "coepigen.o3": "Ælfric's Epilogue to Genesis", + "coeuphr": "Saint Euphrosyne", + "coeust": "Saint Eustace and his companions", + "coexodusP": "Exodus (P)", + "cogenesiC": "Genesis (C)", + "cogregdC.o24": "Gregory's Dialogues (C)", + "cogregdH.o23": "Gregory's Dialogues (H)", + "coherbar": "Pseudo-Apuleius, Herbarium", + "coinspolD.o34": "Wulfstan's Institute of Polity (D)", + "coinspolX": "Wulfstan's Institute of Polity (X)", + "cojames": "Saint James", + "colacnu.o23": "Lacnunga", + "colaece.o2": "Leechdoms", + "colaw1cn.o3": "Laws, Cnut I", + "colaw2cn.o3": "Laws, Cnut II", + "colaw5atr.o3": "Laws, Æthelred V", + "colaw6atr.o3": "Laws, Æthelred VI", + "colawaf.o2": "Laws, Alfred", + "colawafint.o2": "Alfred's Introduction to Laws", + "colawger.o34": "Laws, Gerefa", + "colawine.ox2": "Laws, Ine", + "colawnorthu.o3": "Northumbra Preosta Lagu", + "colawwllad.o4": "Laws, William I, Lad", + "coleofri.o4": "Leofric", + "colsigef.o3": "Ælfric's Letter to Sigefyrth", + "colsigewB": "Ælfric's Letter to Sigeweard (B)", + "colsigewZ.o34": "Ælfric's Letter to Sigeweard (Z)", + "colwgeat": "Ælfric's Letter to Wulfgeat", + "colwsigeT": "Ælfric's Letter to Wulfsige (T)", + "colwsigeXa.o34": "Ælfric's Letter to Wulfsige (Xa)", + "colwstan1.o3": "Ælfric's Letter to Wulfstan I", + "colwstan2.o3": "Ælfric's Letter to Wulfstan II", + "comargaC.o34": "Saint Margaret (C)", + "comargaT": "Saint Margaret (T)", + "comart1": "Martyrology, I", + "comart2": "Martyrology, II", + "comart3.o23": "Martyrology, III", + "comarvel.o23": "Marvels of the East", + "comary": "Mary of Egypt", + "coneot": "Saint Neot", + "conicodA": "Gospel of Nicodemus (A)", + "conicodC": "Gospel of Nicodemus (C)", + "conicodD": "Gospel of Nicodemus (D)", + "conicodE": "Gospel of Nicodemus (E)", + "coorosiu.o2": "Orosius", + "cootest.o3": "Heptateuch", + "coprefcath1.o3": "Ælfric's Preface to Catholic Homilies I", + "coprefcath2.o3": "Ælfric's Preface to Catholic Homilies II", + "coprefcura.o2": "Preface to the Cura Pastoralis", + "coprefgen.o3": "Ælfric's Preface to Genesis", + "copreflives.o3": "Ælfric's Preface to Lives of Saints", + "coprefsolilo": "Preface to Augustine's Soliloquies", + "coquadru.o23": "Pseudo-Apuleius, Medicina de quadrupedibus", + "corood": "History of the Holy Rood-Tree", + "cosevensl": "Seven Sleepers", + "cosolilo": "St. Augustine's Soliloquies", + "cosolsat1.o4": "Solomon and Saturn I", + "cosolsat2": "Solomon and Saturn II", + "cotempo.o3": "Ælfric's De Temporibus Anni", + "coverhom": "Vercelli Homilies", + "coverhomE": "Vercelli Homilies (E)", + "coverhomL": "Vercelli Homilies (L)", + "covinceB": "Saint Vincent (Bodley 343)", + "covinsal": "Vindicta Salvatoris", + "cowsgosp.o3": "West-Saxon Gospels", + "cowulf.o34": "Wulfstan's Homilies", +} diff --git a/nltk/corpus/util.py b/nltk/corpus/util.py new file mode 100644 index 0000000..550aa1e --- /dev/null +++ b/nltk/corpus/util.py @@ -0,0 +1,151 @@ +# Natural Language Toolkit: Corpus Reader Utility Functions +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +###################################################################### +# { Lazy Corpus Loader +###################################################################### + +import gc +import re +import types + +import nltk + +TRY_ZIPFILE_FIRST = False + + +class LazyCorpusLoader: + """ + To see the API documentation for this lazily loaded corpus, first + run corpus.ensure_loaded(), and then run help(this_corpus). + + LazyCorpusLoader is a proxy object which is used to stand in for a + corpus object before the corpus is loaded. This allows NLTK to + create an object for each corpus, but defer the costs associated + with loading those corpora until the first time that they're + actually accessed. + + The first time this object is accessed in any way, it will load + the corresponding corpus, and transform itself into that corpus + (by modifying its own ``__class__`` and ``__dict__`` attributes). + + If the corpus can not be found, then accessing this object will + raise an exception, displaying installation instructions for the + NLTK data package. Once they've properly installed the data + package (or modified ``nltk.data.path`` to point to its location), + they can then use the corpus object without restarting python. + + :param name: The name of the corpus + :type name: str + :param reader_cls: The specific CorpusReader class, e.g. PlaintextCorpusReader, WordListCorpusReader + :type reader: nltk.corpus.reader.api.CorpusReader + :param nltk_data_subdir: The subdirectory where the corpus is stored. + :type nltk_data_subdir: str + :param `*args`: Any other non-keywords arguments that `reader_cls` might need. + :param `**kwargs`: Any other keywords arguments that `reader_cls` might need. + """ + + def __init__(self, name, reader_cls, *args, **kwargs): + from nltk.corpus.reader.api import CorpusReader + + assert issubclass(reader_cls, CorpusReader) + self.__name = self.__name__ = name + self.__reader_cls = reader_cls + # If nltk_data_subdir is set explicitly + if "nltk_data_subdir" in kwargs: + # Use the specified subdirectory path + self.subdir = kwargs["nltk_data_subdir"] + # Pops the `nltk_data_subdir` argument, we don't need it anymore. + kwargs.pop("nltk_data_subdir", None) + else: # Otherwise use 'nltk_data/corpora' + self.subdir = "corpora" + self.__args = args + self.__kwargs = kwargs + + def __load(self): + # Find the corpus root directory. + zip_name = re.sub(r"(([^/]+)(/.*)?)", r"\2.zip/\1/", self.__name) + if TRY_ZIPFILE_FIRST: + try: + root = nltk.data.find(f"{self.subdir}/{zip_name}") + except LookupError as e: + try: + root = nltk.data.find(f"{self.subdir}/{self.__name}") + except LookupError: + raise e + else: + try: + root = nltk.data.find(f"{self.subdir}/{self.__name}") + except LookupError as e: + try: + root = nltk.data.find(f"{self.subdir}/{zip_name}") + except LookupError: + raise e + + # Load the corpus. + corpus = self.__reader_cls(root, *self.__args, **self.__kwargs) + + # This is where the magic happens! Transform ourselves into + # the corpus by modifying our own __dict__ and __class__ to + # match that of the corpus. + + args, kwargs = self.__args, self.__kwargs + name, reader_cls = self.__name, self.__reader_cls + + # Minimal change: avoid swapping out the dict object; update it instead. + self.__dict__.update(corpus.__dict__) + self.__class__ = corpus.__class__ + + # _unload support: assign __dict__ and __class__ back to a fresh + # LazyCorpusLoader proxy. After updating our dict and class, there + # should be no remaining references to the loaded corpus objects, + # making them eligible for collection. + def _unload(self): + # Restore to pristine lazy proxy state without swapping the dict object + fresh = LazyCorpusLoader(name, reader_cls, *args, **kwargs) + self.__class__ = LazyCorpusLoader + self.__dict__.clear() + self.__dict__.update(fresh.__dict__) + + # Bind via helper for flexibility and testability. + self._unload = _make_bound_method(_unload, self) + + def __getattr__(self, attr): + """ + Trigger loading on first missing attribute access. + + Avoid triggering a load for introspection-oriented dunder + attributes (e.g., '__bases__', '__wrapped__'). + """ + if attr.startswith("__") and attr.endswith("__"): + raise AttributeError( + f"{type(self).__name__} object has no attribute {attr!r}" + ) + + self.__load() + # This looks circular, but its not, since __load() changes our + # __class__ to something new: + return getattr(self, attr) + + def __repr__(self): + return "<{} in {!r} (not loaded yet)>".format( + self.__reader_cls.__name__, + ".../corpora/" + self.__name, + ) + + def _unload(self): + # If an exception occurs during corpus loading then + # '_unload' method may be unattached, so __getattr__ can be called; + # we shouldn't trigger corpus loading again in this case. + pass + + +def _make_bound_method(func, self): + """ + Magic for creating bound methods (used for _unload). + """ + return types.MethodType(func, self) diff --git a/nltk/data.py b/nltk/data.py new file mode 100644 index 0000000..0917009 --- /dev/null +++ b/nltk/data.py @@ -0,0 +1,1841 @@ +# Natural Language Toolkit: Utility functions +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Author: ekaf (Restricting and switching pickles) +# URL: +# For license information, see LICENSE.TXT + +""" +Functions to find and load NLTK resource files, such as corpora, +grammars, and saved processing objects. Resource files are identified +using URLs, such as ``nltk:corpora/abc/rural.txt`` or +``https://raw.githubusercontent.com/nltk/nltk/develop/nltk/test/toy.cfg``. +The following URL protocols are supported: + + - ``file:path``: Specifies the file whose path is *path*. + Both relative and absolute paths may be used. + + - ``https://host/path``: Specifies the file stored on the web + server *host* at path *path*. + + - ``nltk:path``: Specifies the file stored in the NLTK data + package at *path*. NLTK will search for these files in the + directories specified by ``nltk.data.path``. + +If no protocol is specified, then the default protocol ``nltk:`` will +be used. + +This module provides to functions that can be used to access a +resource file, given its URL: ``load()`` loads a given resource, and +adds it to a resource cache; and ``retrieve()`` copies a given resource +to a local file. +""" + +import codecs +import functools +import os +import pickle +import re +import sys +import textwrap +import urllib.request +import zipfile +from abc import ABCMeta, abstractmethod +from gzip import WRITE as GZ_WRITE +from gzip import GzipFile +from io import BytesIO, TextIOWrapper +from urllib.parse import unquote +from urllib.request import url2pathname + +from nltk.pathsec import ZipFile +from nltk.pathsec import open as _secure_open +from nltk.pathsec import urlopen as _secure_urlopen + +# Reject unsafe no-protocol paths: traversal segments, trailing '..', absolute paths, +# backslashes, Windows drive letters. Use a raw-string pattern and do not anchor only +# at the start — we'll use search() for safety checks. +_UNSAFE_NO_PROTOCOL_RE = re.compile(r"(?:\.\./|\.\.$|^/|\\|[A-Za-z]:[/\\])") + + +def _assert_no_encoded_bypass(name, error_label=None): + """ + Reject *name* if its URL-decoded form contains an unsafe pattern that + the raw form does not. + + This is the single source of truth for the "did this resource string + smuggle traversal or absolute-path characters past the raw-form + check via percent-encoding?" question. Downstream code applies + :data:`_UNSAFE_NO_PROTOCOL_RE` to the raw resource string, but + :func:`url2pathname` decodes percent-escapes when turning the string + into a filesystem path, so a payload like ``%2fetc%2fpasswd`` would + otherwise pass the raw-form check and then resolve to ``/etc/passwd`` + on disk. Centralising the encoded check here keeps the encoded / + literal policy in lock-step across every call site, which matters + because the rule is security-sensitive and we do not want it to + drift. + + Only a single :func:`unquote` pass is performed, mirroring what + :func:`url2pathname` itself does — decoding repeatedly would change + the meaning of legitimate percent-encoded names such as ``%2520`` + (a literal ``%20``). + + :param name: The resource string to validate. + :param error_label: Optional alternative string to embed in the + ``ValueError`` message (defaults to ``name``). Useful when the + caller wants the error to reference the original outer URL. + """ + decoded = unquote(name) + if decoded != name and _UNSAFE_NO_PROTOCOL_RE.search(decoded): + label = name if error_label is None else error_label + raise ValueError(f"Unsafe resource path: {label!r}") + + +def _reject_unsafe_no_protocol(resource_url): + """ + Reject unsafe resource strings that *omit an explicit protocol*. + + Note: some no-protocol inputs are interpreted by split_resource_url() as + file-style paths (e.g., bare Windows drive paths like "C:/foo"). These must + still be rejected here when they contain unsafe patterns. + + Both the raw and URL-decoded form are validated so that encoded path + separators / traversal segments (``%2f``, ``%2e%2e``, ...) cannot + bypass the filter and later be decoded by :func:`url2pathname` into + a dangerous filesystem path. The encoded-form check is delegated to + :func:`_assert_no_encoded_bypass` to keep the policy in one place. + """ + if _UNSAFE_NO_PROTOCOL_RE.search(resource_url): + raise ValueError(f"Unsafe resource path: {resource_url!r}") + _assert_no_encoded_bypass(resource_url) + + +try: + from zlib import Z_SYNC_FLUSH as FLUSH +except ImportError: + from zlib import Z_FINISH as FLUSH + +from nltk import grammar, sem +from nltk.internals import deprecated + +textwrap_indent = functools.partial(textwrap.indent, prefix=" ") + + +def _is_windows(): + return os.name == "nt" + + +def _windows_data_paths(): + return [ + os.path.join(sys.prefix, "nltk_data"), + os.path.join(sys.prefix, "share", "nltk_data"), + os.path.join(sys.prefix, "lib", "nltk_data"), + os.path.join(os.environ.get("APPDATA", "C:\\"), "nltk_data"), + r"C:\nltk_data", + r"D:\nltk_data", + r"E:\nltk_data", + ] + + +###################################################################### +# Search Path +###################################################################### + +path = [] +"""A list of directories where the NLTK data package might reside. + These directories will be checked in order when looking for a + resource in the data package. Note that this allows users to + substitute in their own versions of resources, if they have them + (e.g., in their home directory under ~/nltk_data).""" + +# User-specified locations: +_paths_from_env = os.environ.get("NLTK_DATA", "").split(os.pathsep) +path += [d for d in _paths_from_env if d] +if "APPENGINE_RUNTIME" not in os.environ and os.path.expanduser("~/") != "~/": + path.append(os.path.expanduser("~/nltk_data")) + +if _is_windows(): + # Common locations on Windows: + path += _windows_data_paths() +else: + # Common locations on UNIX & OS X: + path += [ + os.path.join(sys.prefix, "nltk_data"), + os.path.join(sys.prefix, "share", "nltk_data"), + os.path.join(sys.prefix, "lib", "nltk_data"), + "/usr/share/nltk_data", + "/usr/local/share/nltk_data", + "/usr/lib/nltk_data", + "/usr/local/lib/nltk_data", + ] + + +###################################################################### +# Util Functions +###################################################################### + + +def gzip_open_unicode( + filename, + mode="rb", + compresslevel=9, + encoding="utf-8", + fileobj=None, + errors=None, + newline=None, +): + if fileobj is None: + fileobj = GzipFile(filename, mode, compresslevel, fileobj) + return TextIOWrapper(fileobj, encoding, errors, newline) + + +def split_resource_url(resource_url): + """ + Splits a resource url into ":". + + >>> windows = _is_windows() + >>> split_resource_url('nltk:home/nltk') + ('nltk', 'home/nltk') + >>> split_resource_url('nltk:/home/nltk') + ('nltk', '/home/nltk') + >>> split_resource_url('file:/home/nltk') + ('file', '/home/nltk') + >>> split_resource_url('file:///home/nltk') + ('file', '/home/nltk') + >>> split_resource_url('file:///C:/home/nltk') + ('file', '/C:/home/nltk') + """ + protocol, path_ = resource_url.split(":", 1) + + # Handle plain Windows drive paths like "C:/foo" or "D:/bar" + # Treat these as file-style inputs even without "file:" prefix. + if ( + len(protocol) == 1 + and protocol.isalpha() + and (path_.startswith("/") or path_.startswith("\\")) + ): + return "file", f"/{protocol}:{path_.lstrip('/')}" + + if protocol == "nltk": + pass + elif protocol == "file": + if path_.startswith("/"): + path_ = "/" + path_.lstrip("/") + else: + path_ = re.sub(r"^/{0,2}", "", path_) + + return protocol, path_ + + +def normalize_resource_url(resource_url): + r""" + Normalizes a resource url + + >>> windows = _is_windows() + >>> os.path.normpath(split_resource_url(normalize_resource_url('file:grammar.fcfg'))[1]) == \ + ... ('\\' if windows else '') + os.path.abspath(os.path.join(os.curdir, 'grammar.fcfg')) + True + >>> not windows or normalize_resource_url('file:C:/dir/file') == 'file:///C:/dir/file' + True + >>> not windows or normalize_resource_url('file:C:\\dir\\file') == 'file:///C:/dir/file' + True + >>> not windows or normalize_resource_url('file:C:\\dir/file') == 'file:///C:/dir/file' + True + >>> not windows or normalize_resource_url('file://C:/dir/file') == 'file:///C:/dir/file' + True + >>> not windows or normalize_resource_url('file:////C:/dir/file') == 'file:///C:/dir/file' + True + >>> windows or normalize_resource_url('file:/dir/file/toy.cfg') == 'file:///dir/file/toy.cfg' + True + >>> normalize_resource_url('nltk:home/nltk') + 'nltk:home/nltk' + >>> windows or normalize_resource_url('nltk:/home/nltk') == 'file:///home/nltk' + True + >>> normalize_resource_url('https://example.com/dir/file') + 'https://example.com/dir/file' + >>> normalize_resource_url('dir/file') + 'nltk:dir/file' + + # Security: reject attempts to smuggle local Windows paths via the "nltk:" protocol. + >>> normalize_resource_url('nltk:C:/dir/file') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Unsafe resource path: ... + >>> normalize_resource_url(r'nltk:C:\dir\file') # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: Unsafe resource path: ... + """ + try: + protocol, name = split_resource_url(resource_url) + except ValueError: + # No protocol → default to 'nltk:' + _reject_unsafe_no_protocol(resource_url) + protocol = "nltk" + name = resource_url + # If split_resource_url() inferred "file" from an input that *omitted* an explicit + # protocol (e.g., "C:/dir/file" or "C:\\dir\\file"), then treat it as a no-protocol + # input for security validation to prevent unsafe local path access. + if protocol == "file" and not resource_url.lower().startswith("file:"): + _reject_unsafe_no_protocol(resource_url) + + # ---------------------------------------------------------------------- + # Protocol-specific handling + # ---------------------------------------------------------------------- + + # Case 1: nltk: + if protocol == "nltk": + # Reject encoded-form bypasses (e.g. ``nltk:%2fetc%2fpasswd``) + # before the literal-form routing below interprets the path. The + # encoded check is centralised in _assert_no_encoded_bypass so the + # encoded / literal policy cannot drift across call sites. + _assert_no_encoded_bypass(name, error_label=resource_url) + # Reject Windows drive-letter paths even when explicitly using the + # nltk: protocol. This prevents smuggling filesystem paths through + # nltk: URLs. + if re.match(r"^[A-Za-z]:[/\\]", name): + raise ValueError(f"Unsafe resource path: {resource_url!r}") + # If "nltk:" is used with an absolute path, treat it as "file://" + if os.path.isabs(name): + protocol = "file://" + name = normalize_resource_name(name, False, None) + else: + protocol = "nltk:" + name = normalize_resource_name(name, True) + + # Case 2: file: + elif protocol == "file": + protocol = "file://" + name = normalize_resource_name(name, False, None) + + # Case 3: External URLs (http, https, ftp, etc.) + else: + protocol += "://" + + return protocol + name + + +def normalize_resource_name(resource_name, allow_relative=True, relative_path=None): + """ + :type resource_name: str or unicode + :param resource_name: The name of the resource to search for. + Resource names are posix-style relative path names, such as + ``corpora/brown``. Directory names will automatically + be converted to a platform-appropriate path separator. + Directory trailing slashes are preserved + + >>> windows = _is_windows() + >>> normalize_resource_name('.', True) + './' + >>> normalize_resource_name('./', True) + './' + >>> windows or normalize_resource_name('dir/file', False, '/') == '/dir/file' + True + >>> not windows or normalize_resource_name('C:/file', False, '/') == '/C:/file' + True + >>> windows or normalize_resource_name('/dir/file', False, '/') == '/dir/file' + True + >>> windows or normalize_resource_name('../dir/file', False, '/') == '/dir/file' + True + >>> not windows or normalize_resource_name('/dir/file', True, '/') == 'dir/file' + True + >>> windows or normalize_resource_name('/dir/file', True, '/') == '/dir/file' + True + """ + is_dir = bool(re.search(r"[\\/.]$", resource_name)) or resource_name.endswith( + os.path.sep + ) + if _is_windows(): + resource_name = resource_name.lstrip("/") + else: + resource_name = re.sub(r"^/+", "/", resource_name) + if allow_relative: + resource_name = os.path.normpath(resource_name) + else: + if relative_path is None: + relative_path = os.curdir + resource_name = os.path.abspath(os.path.join(relative_path, resource_name)) + resource_name = resource_name.replace("\\", "/").replace(os.path.sep, "/") + if _is_windows() and os.path.isabs(resource_name): + resource_name = "/" + resource_name + if is_dir and not resource_name.endswith("/"): + resource_name += "/" + return resource_name + + +###################################################################### +# Path Pointers +###################################################################### + + +class PathPointer(metaclass=ABCMeta): + """ + An abstract base class for 'path pointers,' used by NLTK's data + package to identify specific paths. Two subclasses exist: + ``FileSystemPathPointer`` identifies a file that can be accessed + directly via a given absolute path. ``ZipFilePathPointer`` + identifies a file contained within a zipfile, that can be accessed + by reading that zipfile. + """ + + @abstractmethod + def open(self, encoding=None): + """ + Return a seekable read-only stream that can be used to read + the contents of the file identified by this path pointer. + + :raise IOError: If the path specified by this pointer does + not contain a readable file. + """ + + @abstractmethod + def file_size(self): + """ + Return the size of the file pointed to by this path pointer, + in bytes. + + :raise IOError: If the path specified by this pointer does + not contain a readable file. + """ + + @abstractmethod + def join(self, fileid): + """ + Return a new path pointer formed by starting at the path + identified by this pointer, and then following the relative + path given by ``fileid``. The path components of ``fileid`` + should be separated by forward slashes, regardless of + the underlying file system's path separator character. + """ + + +class FileSystemPathPointer(PathPointer, str): + """ + A path pointer that identifies a file which can be accessed + directly via a given absolute path. + """ + + def __init__(self, _path): + """ + Create a new path pointer for the given absolute path. + + :raise IOError: If the given path does not exist. + """ + + _path = os.path.abspath(_path) + if not os.path.exists(_path): + raise OSError("No such file or directory: %r" % _path) + self._path = _path + + # There's no need to call str.__init__(), since it's a no-op; + # str does all of its setup work in __new__. + + @property + def path(self): + """The absolute path identified by this path pointer.""" + return self._path + + def open(self, encoding=None): + """ + Secure open — prevents absolute direct access outside pointer root. + Path validation is enforced by pathsec.open() which checks the + resolved path against allowed NLTK data roots. + """ + stream = _secure_open(self._path, "rb") + if encoding is not None: + stream = SeekableUnicodeStreamReader(stream, encoding) + return stream + + def file_size(self): + return os.stat(self._path).st_size + + def join(self, fileid): + """ + Harden join() to prevent traversal & ensure corpus-root sandbox. + """ + fileid = str(fileid).replace("\\", "/") + + # Block ../ traversal + if ".." in fileid.split("/"): + raise ValueError(f"Traversal blocked: {fileid}") + + joined = os.path.normpath(os.path.join(self._path, fileid)) + root = os.path.normpath(self._path) + + # Enforce root boundary — must stay inside corpus root + if not (joined == root or joined.startswith(root + os.sep)): + raise ValueError(f"Escape outside root blocked: {joined}") + + return FileSystemPathPointer(joined) + + def __repr__(self): + return "FileSystemPathPointer(%r)" % self._path + + def __str__(self): + return self._path + + +@deprecated("Use gzip.GzipFile instead as it also uses a buffer.") +class BufferedGzipFile(GzipFile): + """A ``GzipFile`` subclass for compatibility with older nltk releases. + + Use ``GzipFile`` directly as it also buffers in all supported + Python versions. + """ + + def __init__( + self, filename=None, mode=None, compresslevel=9, fileobj=None, **kwargs + ): + """Return a buffered gzip file object.""" + GzipFile.__init__(self, filename, mode, compresslevel, fileobj) + + def write(self, data): + # This is identical to GzipFile.write but does not return + # the bytes written to retain compatibility. + super().write(data) + + +class GzipFileSystemPathPointer(FileSystemPathPointer): + """ + A subclass of ``FileSystemPathPointer`` that identifies a gzip-compressed + file located at a given absolute path. ``GzipFileSystemPathPointer`` is + appropriate for loading large gzip-compressed pickle objects efficiently. + """ + + def open(self, encoding=None): + # Route through the sentinel like FileSystemPathPointer.open() so the + # path is validated against the allowed NLTK data roots (with symlinks + # resolved) before reading; decompress the validated stream rather than + # re-opening the path directly (CWE-22 / CWE-73). + stream = GzipFile(self._path, fileobj=_secure_open(self._path, "rb")) + # ``encoding is not None`` (not truthiness) to match + # FileSystemPathPointer.open() / ZipFilePathPointer.open(). + if encoding is not None: + stream = SeekableUnicodeStreamReader(stream, encoding) + return stream + + +#: Maximum allowed ratio of a zip member's uncompressed size to its stored +#: (compressed) size before it is treated as a decompression bomb (CWE-409). +#: DEFLATE's maximum ratio is ~1032x, reached only by runs of identical bytes, +#: i.e. payloads crafted purely to maximize expansion; legitimate text/markup +#: corpora compress a few-fold to a few-hundred-fold at most, well under this. +#: A member that expands beyond this ratio is rejected. Configurable. +MAX_UNZIP_RATIO = 1000 + +#: Optional hard cap (in bytes) on a single zip member's uncompressed size. +#: ``None`` disables it (the default, so legitimately large corpora are not +#: affected); set it for a strict absolute limit in hardened deployments. +MAX_UNZIP_SIZE = None + +#: The ratio check is only applied once a member's declared uncompressed size +#: exceeds this activation threshold, so small files (which cannot exhaust +#: resources regardless of ratio) are never rejected. +MAX_UNZIP_ACTIVATION = 32 * 1024 * 1024 # 32 MiB + + +def _check_decompression_bomb(info): + """ + Reject a zip member that looks like a decompression bomb (CWE-409). + + ``info`` is a ``zipfile.ZipInfo``. A member is refused when its declared + uncompressed size exceeds the optional hard cap ``MAX_UNZIP_SIZE``, or when + that size is both large (>= ``MAX_UNZIP_ACTIVATION``) and expands by more + than ``MAX_UNZIP_RATIO`` over its stored compressed size. This guards the + read-into-memory paths (``ZipFilePathPointer.open``, + ``OpenOnDemandZipFile.read``) and the on-disk extraction path + (``nltk.downloader``) against tiny archives that exhaust RAM or disk. + """ + compress_size = getattr(info, "compress_size", 0) or 0 + file_size = getattr(info, "file_size", 0) or 0 + name = getattr(info, "filename", "") + + if MAX_UNZIP_SIZE is not None and file_size > MAX_UNZIP_SIZE: + raise ValueError( + "Refusing to decompress zip member %r: uncompressed size %d bytes " + "exceeds nltk.data.MAX_UNZIP_SIZE=%d." % (name, file_size, MAX_UNZIP_SIZE) + ) + + if ( + file_size >= MAX_UNZIP_ACTIVATION + and compress_size > 0 + # integer comparison (no float rounding at the threshold) + and file_size > MAX_UNZIP_RATIO * compress_size + ): + raise ValueError( + "Refusing to decompress suspected zip bomb %r: it expands %.1fx " + "(%d -> %d bytes), above nltk.data.MAX_UNZIP_RATIO=%d. " + "Raise nltk.data.MAX_UNZIP_RATIO if this data is trusted." + % ( + name, + file_size / compress_size, + compress_size, + file_size, + MAX_UNZIP_RATIO, + ) + ) + + +class ZipFilePathPointer(PathPointer): + """ + A path pointer that identifies a file contained within a zipfile, + which can be accessed by reading that zipfile. + """ + + def __init__(self, zipfile, entry=""): + """ + Create a new path pointer pointing at the specified entry + in the given zipfile. + + :raise IOError: If the given zipfile does not exist, or if it + does not contain the specified entry. + """ + if isinstance(zipfile, str): + zipfile = OpenOnDemandZipFile(os.path.abspath(zipfile)) + + # Check that the entry exists: + if entry: + # Normalize the entry string, it should be relative: + entry = normalize_resource_name(entry, True, "/").lstrip("/") + + try: + zipfile.getinfo(entry) + except Exception as e: + # Sometimes directories aren't explicitly listed in + # the zip file. So if `entry` is a directory name, + # then check if the zipfile contains any files that + # are under the given directory. + if entry.endswith("/") and [ + n for n in zipfile.namelist() if n.startswith(entry) + ]: + pass # zipfile contains a file in that directory. + else: + # Otherwise, complain. + raise OSError( + f"Zipfile {zipfile.filename!r} does not contain {entry!r}" + ) from e + self._zipfile = zipfile + self._entry = entry + + @property + def zipfile(self): + """ + The ZipFile object used to access the zip file + containing the entry identified by this path pointer. + """ + return self._zipfile + + @property + def entry(self): + """ + The name of the file within zipfile that this path + pointer points to. + """ + return self._entry + + def open(self, encoding=None): + _check_decompression_bomb(self._zipfile.getinfo(self._entry)) + data = self._zipfile.read(self._entry) + stream = BytesIO(data) + if self._entry.endswith(".gz"): + stream = GzipFile(self._entry, fileobj=stream) + elif encoding is not None: + stream = SeekableUnicodeStreamReader(stream, encoding) + return stream + + def file_size(self): + return self._zipfile.getinfo(self._entry).file_size + + def join(self, fileid): + entry = f"{self._entry}/{fileid}" + return ZipFilePathPointer(self._zipfile, entry) + + def __repr__(self): + return f"ZipFilePathPointer({self._zipfile.filename!r}, {self._entry!r})" + + def __str__(self): + return os.path.normpath(os.path.join(self._zipfile.filename, self._entry)) + + +###################################################################### +# Access Functions +###################################################################### + +# Don't use a weak dictionary, because in the common case this +# causes a lot more reloading that necessary. +_resource_cache = {} +"""A dictionary used to cache resources so that they won't + need to be loaded more than once.""" + + +def open_datafile(path, file_name="", encoding="utf-8"): + """ + Open a data file using a PathPointer, supporting both filesystem and zip file paths. + + The function can be used in two ways: + + 1. `path` is a PathPointer to a directory, and `file_name` is the name of a file + within that directory. + 2. `path` is a PathPointer to a file, and `file_name` is left empty. + + :param path: A PathPointer (e.g. FileSystemPathPointer or ZipFilePathPointer) + representing either the file to open (when file_name is empty), or the + directory containing the file. + :type path: PathPointer + :param file_name: The name of the file to open within the directory. Leave empty + if `path` already points to the file. + :type file_name: str + :param encoding: The character encoding to use when opening the file. If None, + a binary stream is returned. + :type encoding: str or None + :return: A file-like object (binary stream if encoding is None, otherwise a text + stream with the specified encoding). + :rtype: file-like + """ + if file_name: + # Use .join() to reach the file regardless of zip/real FS. + path = path.join(file_name) + return path.open(encoding=encoding) + + +def find(resource_name, paths=None): + """ + Find the given resource by searching through the directories and + zip files in paths, where a None or empty string specifies an absolute path. + Returns a corresponding path name. If the given resource is not + found, raise a ``LookupError``, whose message gives a pointer to + the installation instructions for the NLTK downloader. + + Zip File Handling: + + - If ``resource_name`` contains a component with a ``.zip`` + extension, then it is assumed to be a zipfile; and the + remaining path components are used to look inside the zipfile. + + - If any element of ``nltk.data.path`` has a ``.zip`` extension, + then it is assumed to be a zipfile. + + - If a given resource name that does not contain any zipfile + component is not found initially, then ``find()`` will make a + second attempt to find that resource, by replacing each + component *p* in the path with *p.zip/p*. For example, this + allows ``find()`` to map the resource name + ``corpora/chat80/cities.pl`` to a zip file path pointer to + ``corpora/chat80.zip/chat80/cities.pl``. + + - When using ``find()`` to locate a directory contained in a + zipfile, the resource name must end with the forward slash + character. Otherwise, ``find()`` will not locate the + directory. + + :type resource_name: str or unicode + :param resource_name: The name of the resource to search for. + Resource names are posix-style relative path names, such as + ``corpora/brown``. Directory names will be + automatically converted to a platform-appropriate path separator. + :rtype: str + """ + resource_name = normalize_resource_name(resource_name, True) + # Defense-in-depth: reject traversal/absolute paths even if a caller + # bypassed normalize_resource_url(). Use search() so traversal + # components anywhere in the resource_name trigger rejection. The + # URL-decoded form is checked via _assert_no_encoded_bypass to keep + # the encoded / literal policy aligned with the other call sites. + if _UNSAFE_NO_PROTOCOL_RE.search(resource_name): + raise ValueError(f"Unsafe resource path: {resource_name!r}") + _assert_no_encoded_bypass(resource_name) + + # Resolve default paths at runtime in-case the user overrides + # nltk.data.path + if paths is None: + paths = path + + # Check if the resource name includes a zipfile name + m = re.match(r"(.*?\.zip)/?(.*)$", resource_name) + if m: + zipfile, zipentry = m.groups() + else: + zipfile = None + + # Evidence that the *package* exists but the specific entry does not. + _package_present_but_entry_missing = [] + + def _note_near_miss(where): + if where not in _package_present_but_entry_missing: + _package_present_but_entry_missing.append(where) + + # Check each item in our path + for path_ in paths: + # Is the path item a zipfile? + if path_ and (os.path.isfile(path_) and path_.endswith(".zip")): + try: + return ZipFilePathPointer(path_, resource_name) + except OSError: + # resource not in zipfile + _note_near_miss(path_) + continue + + # Is the path item a directory or is resource_name an absolute path? + elif not path_ or os.path.isdir(path_): + if zipfile is None: + p = os.path.join(path_, url2pathname(resource_name)) + if os.path.exists(p): + if p.endswith(".gz"): + return GzipFileSystemPathPointer(p) + else: + return FileSystemPathPointer(p) + else: + # If the package exists (either as a directory or as a .zip) + # but the specific requested file doesn't, record a "near miss" + # so the eventual LookupError isn't misleading. + parts = [p for p in resource_name.split("/") if p] + # Only record a "near miss" when there is a sub-entry *within* a + # package (i.e. more than two meaningful path components), so we + # don't misclassify requests for the package root itself. + if len(parts) > 2: + pkg = "/".join(parts[:2]) # e.g. "corpora/stopwords" + pkg_dir = os.path.join(path_, url2pathname(pkg)) + pkg_zip = os.path.join(path_, url2pathname(pkg + ".zip")) + if os.path.isdir(pkg_dir): + _note_near_miss(pkg_dir) + elif os.path.isfile(pkg_zip): + _note_near_miss(pkg_zip) + else: + p = os.path.join(path_, url2pathname(zipfile)) + if os.path.exists(p): + try: + return ZipFilePathPointer(p, zipentry) + except OSError: + # resource not in zipfile + _note_near_miss(p) + continue + + # Fallback: if the path doesn't include a zip file, then try + # again, assuming that one of the path components is inside a + # zipfile of the same name. + if zipfile is None: + pieces = resource_name.split("/") + for i in range(len(pieces)): + modified_name = "/".join(pieces[:i] + [pieces[i] + ".zip"] + pieces[i:]) + try: + return find(modified_name, paths) + except LookupError: + pass + + # Identify the package (i.e. the .zip file) to download. + parts = resource_name.split("/") + resource_zipname = parts[1] if len(parts) > 1 else parts[0] + if resource_zipname.endswith(".zip"): + resource_zipname = resource_zipname.rpartition(".")[0] + + # HuggingFace fallback: if the corpus is in the HF registry and has + # already been downloaded to the HF datasets cache, serve it from there. + # Uses local_files_only=True so no network request is made here. + try: + from nltk.huggingface.dataset import REGISTRY, HFDatasetPathPointer, _is_cached + + if resource_zipname in REGISTRY and _is_cached(resource_zipname): + return HFDatasetPathPointer(resource_zipname) + except ImportError: + pass + + # Display a friendly error message if the resource wasn't found. + # If the package appears present but the specific entry is missing, keep + # the download hint as a secondary suggestion. + if _package_present_but_entry_missing: + msg = ( + f"Resource entry '{resource_name}' not found in installed package " + f"'{resource_zipname}'.\n" + "The package appears to be installed, but the requested file is missing.\n" + "\n" + "If you believe the package is corrupted or out of date, you can try " + "re-downloading it with the NLTK Downloader:\n\n" + ">>> import nltk\n" + f">>> nltk.download('{resource_zipname}')\n" + ) + else: + msg = ( + f"Resource '{resource_zipname}' not found.\n" + "Please use the NLTK Downloader to obtain the resource:\n\n" + ">>> import nltk\n" + f">>> nltk.download('{resource_zipname}')\n" + ) + msg = textwrap_indent(msg) + + msg += "\n For more information see: https://www.nltk.org/data.html\n" + + msg += f"\n Attempted to load '{resource_name}'\n" + + if _package_present_but_entry_missing: + msg += "\n Package was found in:" + "".join( + "\n - %r" % d for d in _package_present_but_entry_missing + ) + + msg += "\n Searched in:" + "".join("\n - %r" % d for d in paths) + sep = "*" * 70 + resource_not_found = f"\n{sep}\n{msg}\n{sep}\n" + raise LookupError(resource_not_found) + + +def retrieve(resource_url, filename=None, verbose=True): + """ + Copy the given resource to a local file. If no filename is + specified, then use the URL's filename. If there is already a + file named ``filename``, then raise a ``ValueError``. + + :type resource_url: str + :param resource_url: A URL specifying where the resource should be + loaded from. The default protocol is "nltk:", which searches + for the file in the the NLTK data package. + """ + resource_url = normalize_resource_url(resource_url) + if filename is None: + if resource_url.startswith("file:"): + filename = os.path.split(resource_url)[-1] + else: + filename = re.sub(r"(^\w+:)?.*/", "", resource_url) + if os.path.exists(filename): + filename = os.path.abspath(filename) + raise ValueError("File %r already exists!" % filename) + + if verbose: + print(f"Retrieving {resource_url!r}, saving to {filename!r}") + + # Open the input & output streams. + infile = _open(resource_url) + + # Copy infile -> outfile, using 64k blocks. Route the write through the + # pathsec sentinel so the destination honours the file-access sandbox + # (allowed data roots, symlink resolution) instead of the builtin open, + # which bypasses it and can write arbitrary local files -- an arbitrary + # file write that can escalate to code execution via a planted file + # (CWE-22). + with _secure_open(filename, "wb") as outfile: + while True: + s = infile.read(1024 * 64) # 64k blocks. + outfile.write(s) + if not s: + break + + infile.close() + + +#: A dictionary describing the formats that are supported by NLTK's +#: load() method. Keys are format names, and values are format +#: descriptions. +FORMATS = { + "pickle": "A serialized python object, stored using the pickle module.", + "json": "A serialized python object, stored using the json module.", + "yaml": "A serialized python object, stored using the yaml module.", + "cfg": "A context free grammar.", + "pcfg": "A probabilistic CFG.", + "fcfg": "A feature CFG.", + "fol": "A list of first order logic expressions, parsed with " + "nltk.sem.logic.Expression.fromstring.", + "logic": "A list of first order logic expressions, parsed with " + "nltk.sem.logic.LogicParser. Requires an additional logic_parser " + "parameter", + "val": "A semantic valuation, parsed by nltk.sem.Valuation.fromstring.", + "raw": "The raw (byte string) contents of a file.", + "text": "The raw (unicode string) contents of a file. ", +} + +#: A dictionary mapping from file extensions to format names, used +#: by load() when format="auto" to decide the format for a +#: given resource url. +AUTO_FORMATS = { + "pickle": "pickle", + "json": "json", + "yaml": "yaml", + "cfg": "cfg", + "pcfg": "pcfg", + "fcfg": "fcfg", + "fol": "fol", + "logic": "logic", + "val": "val", + "txt": "text", + "text": "text", +} + + +def restricted_pickle_load(string): + """ + Prevents any class or function from loading. + """ + from nltk.picklesec import RestrictedUnpickler + + return RestrictedUnpickler(BytesIO(string)).load() + + +def switch_punkt(lang="english"): + """ + Return a pickle-free Punkt tokenizer instead of loading a pickle. + + >>> import nltk + >>> tokenizer = nltk.data.load('tokenizers/punkt/english.pickle') + >>> print(tokenizer.tokenize("Hello! How are you?")) + ['Hello!', 'How are you?'] + """ + from nltk.tokenize import PunktTokenizer as tok + + return tok(lang) + + +def switch_chunker(fmt="multiclass"): + """ + Return a pickle-free Named Entity Chunker instead of loading a pickle. + + >>> import nltk + >>> from nltk.corpus import treebank + >>> from pprint import pprint + >>> chunker = nltk.data.load('chunkers/maxent_ne_chunker/PY3/english_ace_multiclass.pickle') + >>> pprint(chunker.parse(treebank.tagged_sents()[2][8:14])) # doctest: +NORMALIZE_WHITESPACE + Tree('S', [('chairman', 'NN'), ('of', 'IN'), Tree('ORGANIZATION', [('Consolidated', 'NNP'), ('Gold', 'NNP'), ('Fields', 'NNP')]), ('PLC', 'NNP')]) + + """ + from nltk.chunk import ne_chunker + + return ne_chunker(fmt) + + +def switch_t_tagger(): + """ + Return a pickle-free Treebank Pos Tagger instead of loading a pickle. + + >>> import nltk + >>> from nltk.tokenize import word_tokenize + >>> tagger = nltk.data.load('taggers/maxent_treebank_pos_tagger/PY3/english.pickle') + >>> print(tagger.tag(word_tokenize("Hello, how are you?"))) + [('Hello', 'NNP'), (',', ','), ('how', 'WRB'), ('are', 'VBP'), ('you', 'PRP'), ('?', '.')] + + """ + from nltk.classify.maxent import maxent_pos_tagger + + return maxent_pos_tagger() + + +def switch_p_tagger(lang): + """ + Return a pickle-free Averaged Perceptron Tagger instead of loading a pickle. + + >>> import nltk + >>> from nltk.tokenize import word_tokenize + >>> tagger = nltk.data.load('taggers/averaged_perceptron_tagger/averaged_perceptron_tagger.pickle') + >>> print(tagger.tag(word_tokenize("Hello, how are you?"))) + [('Hello', 'NNP'), (',', ','), ('how', 'WRB'), ('are', 'VBP'), ('you', 'PRP'), ('?', '.')] + + """ + from nltk.tag import _get_tagger + + if lang == "ru": + lang = "rus" + else: + lang = None + return _get_tagger(lang) + + +def load( + resource_url, + format="auto", + cache=True, + verbose=False, + logic_parser=None, + fstruct_reader=None, + encoding=None, +): + """ + Load a given resource from the NLTK data package. The following + resource formats are currently supported: + + - ``pickle`` + - ``json`` + - ``yaml`` + - ``cfg`` (context free grammars) + - ``pcfg`` (probabilistic CFGs) + - ``fcfg`` (feature-based CFGs) + - ``fol`` (formulas of First Order Logic) + - ``logic`` (Logical formulas to be parsed by the given logic_parser) + - ``val`` (valuation of First Order Logic model) + - ``text`` (the file contents as a unicode string) + - ``raw`` (the raw file contents as a byte string) + + If no format is specified, ``load()`` will attempt to determine a + format based on the resource name's file extension. If that + fails, ``load()`` will raise a ``ValueError`` exception. + + For all text formats (everything except ``pickle``, ``json``, ``yaml`` and ``raw``), + it tries to decode the raw contents using UTF-8, and if that doesn't + work, it tries with ISO-8859-1 (Latin-1), unless the ``encoding`` + is specified. + + :type resource_url: str + :param resource_url: A URL specifying where the resource should be + loaded from. The default protocol is "nltk:", which searches + for the file in the the NLTK data package. + :type cache: bool + :param cache: If true, add this resource to a cache. If load() + finds a resource in its cache, then it will return it from the + cache rather than loading it. + :type verbose: bool + :param verbose: If true, print a message when loading a resource. + Messages are not displayed when a resource is retrieved from + the cache. + :type logic_parser: LogicParser + :param logic_parser: The parser that will be used to parse logical + expressions. + :type fstruct_reader: FeatStructReader + :param fstruct_reader: The parser that will be used to parse the + feature structure of an fcfg. + :type encoding: str + :param encoding: the encoding of the input; only used for text formats. + """ + resource_url = normalize_resource_url(resource_url) + + # Determine the format of the resource. + if format == "auto": + resource_url_parts = resource_url.split(".") + ext = resource_url_parts[-1] + if ext == "gz": + ext = resource_url_parts[-2] + format = AUTO_FORMATS.get(ext) + if format is None: + raise ValueError( + "Could not determine format for %s based " + 'on its file\nextension; use the "format" ' + "argument to specify the format explicitly." % resource_url + ) + + if format not in FORMATS: + raise ValueError(f"Unknown format type: {format}!") + + # If we've cached the resource, then just return it. + if cache: + resource_val = _resource_cache.get((resource_url, format)) + if resource_val is not None: + if verbose: + print(f"<>") + return resource_val + + protocol, path_ = split_resource_url(resource_url) + + if path_[-7:] == ".pickle": + if verbose: + print(f"<>") + fil = os.path.split(path_[:-7])[-1] + if path_.startswith("tokenizers/punkt"): + return switch_punkt(fil) + elif path_.startswith("chunkers/maxent_ne_chunker"): + return switch_chunker(fil.split("_")[-1]) + elif path_.startswith("taggers/maxent_treebank_pos_tagger"): + return switch_t_tagger() + elif path_.startswith("taggers/averaged_perceptron_tagger"): + return switch_p_tagger(fil.split("_")[-1]) + + # Let the user know what's going on. + if verbose: + print(f"<>") + + # Load the resource. + opened_resource = _open(resource_url) + + if format == "raw": + resource_val = opened_resource.read() + elif format == "pickle": + resource_val = restricted_pickle_load(opened_resource.read()) + elif format == "json": + import json + + from nltk.jsontags import json_tags + + resource_val = json.load(opened_resource) + tag = None + if len(resource_val) != 1: + tag = next(resource_val.keys()) + if tag not in json_tags: + raise ValueError("Unknown json tag.") + elif format == "yaml": + import yaml + + resource_val = yaml.safe_load(opened_resource) + else: + # The resource is a text format. + binary_data = opened_resource.read() + if encoding is not None: + string_data = binary_data.decode(encoding) + else: + try: + string_data = binary_data.decode("utf-8") + except UnicodeDecodeError: + string_data = binary_data.decode("latin-1") + if format == "text": + resource_val = string_data + elif format == "cfg": + resource_val = grammar.CFG.fromstring(string_data, encoding=encoding) + elif format == "pcfg": + resource_val = grammar.PCFG.fromstring(string_data, encoding=encoding) + elif format == "fcfg": + resource_val = grammar.FeatureGrammar.fromstring( + string_data, + logic_parser=logic_parser, + fstruct_reader=fstruct_reader, + encoding=encoding, + ) + elif format == "fol": + resource_val = sem.read_logic( + string_data, + logic_parser=sem.logic.LogicParser(), + encoding=encoding, + ) + elif format == "logic": + resource_val = sem.read_logic( + string_data, logic_parser=logic_parser, encoding=encoding + ) + elif format == "val": + resource_val = sem.read_valuation(string_data, encoding=encoding) + else: + raise AssertionError( + "Internal NLTK error: Format %s isn't " + "handled by nltk.data.load()" % (format,) + ) + + opened_resource.close() + + # If requested, add it to the cache. + if cache: + try: + _resource_cache[(resource_url, format)] = resource_val + # TODO: add this line + # print('<>' % (resource_url,)) + except TypeError: + # We can't create weak references to some object types, like + # strings and tuples. For now, just don't cache them. + pass + + return resource_val + + +def show_cfg(resource_url, escape="##"): + """ + Write out a grammar file, ignoring escaped and empty lines. + + :type resource_url: str + :param resource_url: A URL specifying where the resource should be + loaded from. The default protocol is "nltk:", which searches + for the file in the the NLTK data package. + :type escape: str + :param escape: Prepended string that signals lines to be ignored + """ + resource_url = normalize_resource_url(resource_url) + resource_val = load(resource_url, format="text", cache=False) + lines = resource_val.splitlines() + for l in lines: + if l.startswith(escape): + continue + if re.match("^$", l): + continue + print(l) + + +def clear_cache(): + """ + Remove all objects from the resource cache. + :see: load() + """ + _resource_cache.clear() + + +def _open(resource_url): + """ + Helper function that returns an open file object for a resource, + given its resource URL. If the given resource URL uses the "nltk:" + protocol, or uses no protocol, then use ``nltk.data.find`` to find + its path, and open it with the given mode; if the resource URL + uses the 'file' protocol, then open the file with the given mode; + otherwise, delegate to ``urllib2.urlopen``. + + :type resource_url: str + :param resource_url: A URL specifying where the resource should be + loaded from. The default protocol is "nltk:", which searches + for the file in the the NLTK data package. + """ + # Restore "no protocol" handling for internal resilience + resource_url = str(resource_url) + if ":" not in resource_url: + resource_url = "nltk:" + resource_url + + protocol, path_ = resource_url.split(":", 1) + + if protocol == "nltk": + # If find() or .open() raises a ValueError (security) or LookupError, + # let it bubble up or handle it based on load() logic. + return find(path_).open() + elif protocol == "file": + local_path = url2pathname(path_) + try: + # 1. Attempt to use NLTK's standard search paths (Safe/Normalized) + return find(local_path).open() + except (LookupError, ValueError): + # 2. Fallback for absolute paths (e.g., file:///etc/passwd) + # This ensures even direct file access hits the pathsec sentinel. + return _secure_open(local_path, "rb") + else: + # Network protocols (http, https, ftp) + return _secure_urlopen(resource_url) + + +###################################################################### +# Lazy Resource Loader +###################################################################### + + +class LazyLoader: + + def __init__(self, _path): + self._path = _path + + def __load(self): + resource = load(self._path) + # This is where the magic happens! Transform ourselves into + # the object by modifying our own __dict__ and __class__ to + # match that of `resource`. + self.__dict__ = resource.__dict__ + self.__class__ = resource.__class__ + + def __getattr__(self, attr): + self.__load() + # This looks circular, but its not, since __load() changes our + # __class__ to something new: + return getattr(self, attr) + + def __repr__(self): + self.__load() + # This looks circular, but its not, since __load() changes our + # __class__ to something new: + return repr(self) + + +###################################################################### +# Open-On-Demand ZipFile +###################################################################### + + +class OpenOnDemandZipFile(ZipFile): + """ + A subclass of ``ZipFile`` that closes its file pointer + whenever it is not using it; and re-opens it when it needs to read + data from the zipfile. This is useful for reducing the number of + open file handles when many zip files are being accessed at once. + ``OpenOnDemandZipFile`` must be constructed from a filename, not a + file-like object (to allow re-opening). ``OpenOnDemandZipFile`` is + read-only (i.e. ``write()`` and ``writestr()`` are disabled. + """ + + def __init__(self, filename): + if not isinstance(filename, str): + raise TypeError("ReopenableZipFile filename must be a string") + ZipFile.__init__(self, filename) + assert self.filename == filename + self.close() + # After closing a ZipFile object, the _fileRefCnt needs to be cleared + # for Python2and3 compatible code. + self._fileRefCnt = 0 + + def read(self, name): + assert self.fp is None + _check_decompression_bomb(self.getinfo(name)) + # This will be validated by pathsec.open + self.fp = _secure_open(self.filename, "rb") + value = zipfile.ZipFile.read(self, name) + self.fp.close() + self.fp = None + return value + + def write(self, *args, **kwargs): + """:raise NotImplementedError: OpenOnDemandZipfile is read-only""" + raise NotImplementedError("OpenOnDemandZipfile is read-only") + + def writestr(self, *args, **kwargs): + """:raise NotImplementedError: OpenOnDemandZipfile is read-only""" + raise NotImplementedError("OpenOnDemandZipfile is read-only") + + def __repr__(self): + return repr("OpenOnDemandZipFile(%r)" % self.filename) + + +###################################################################### +# Seekable Unicode Stream Reader +###################################################################### + + +class SeekableUnicodeStreamReader: + """ + A stream reader that automatically encodes the source byte stream + into unicode (like ``codecs.StreamReader``); but still supports the + ``seek()`` and ``tell()`` operations correctly. This is in contrast + to ``codecs.StreamReader``, which provide *broken* ``seek()`` and + ``tell()`` methods. + + This class was motivated by ``StreamBackedCorpusView``, which + makes extensive use of ``seek()`` and ``tell()``, and needs to be + able to handle unicode-encoded files. + + Note: this class requires stateless decoders. To my knowledge, + this shouldn't cause a problem with any of python's builtin + unicode encodings. + """ + + DEBUG = True # : If true, then perform extra sanity checks. + + def __init__(self, stream, encoding, errors="strict"): + # Rewind the stream to its beginning. + stream.seek(0) + + self.stream = stream + """The underlying stream.""" + + self.encoding = encoding + """The name of the encoding that should be used to encode the + underlying stream.""" + + self.errors = errors + """The error mode that should be used when decoding data from + the underlying stream. Can be 'strict', 'ignore', or + 'replace'.""" + + self.decode = codecs.getdecoder(encoding) + """The function that is used to decode byte strings into + unicode strings.""" + + self.bytebuffer = b"" + """A buffer to use bytes that have been read but have not yet + been decoded. This is only used when the final bytes from + a read do not form a complete encoding for a character.""" + + self.linebuffer = None + """A buffer used by ``readline()`` to hold characters that have + been read, but have not yet been returned by ``read()`` or + ``readline()``. This buffer consists of a list of unicode + strings, where each string corresponds to a single line. + The final element of the list may or may not be a complete + line. Note that the existence of a linebuffer makes the + ``tell()`` operation more complex, because it must backtrack + to the beginning of the buffer to determine the correct + file position in the underlying byte stream.""" + + self._rewind_checkpoint = 0 + """The file position at which the most recent read on the + underlying stream began. This is used, together with + ``_rewind_numchars``, to backtrack to the beginning of + ``linebuffer`` (which is required by ``tell()``).""" + + self._rewind_numchars = None + """The number of characters that have been returned since the + read that started at ``_rewind_checkpoint``. This is used, + together with ``_rewind_checkpoint``, to backtrack to the + beginning of ``linebuffer`` (which is required by ``tell()``).""" + + self._bom = self._check_bom() + """The length of the byte order marker at the beginning of + the stream (or None for no byte order marker).""" + + # ///////////////////////////////////////////////////////////////// + # Read methods + # ///////////////////////////////////////////////////////////////// + + def read(self, size=None): + """ + Read up to ``size`` bytes, decode them using this reader's + encoding, and return the resulting unicode string. + + :param size: The maximum number of bytes to read. If not + specified, then read as many bytes as possible. + :type size: int + :rtype: unicode + """ + chars = self._read(size) + + # If linebuffer is not empty, then include it in the result + if self.linebuffer: + chars = "".join(self.linebuffer) + chars + self.linebuffer = None + self._rewind_numchars = None + + return chars + + def discard_line(self): + if self.linebuffer and len(self.linebuffer) > 1: + line = self.linebuffer.pop(0) + self._rewind_numchars += len(line) + else: + self.stream.readline() + + def readline(self, size=None): + """ + Read a line of text, decode it using this reader's encoding, + and return the resulting unicode string. + + :param size: The maximum number of bytes to read. If no + newline is encountered before ``size`` bytes have been read, + then the returned value may not be a complete line of text. + :type size: int + """ + # If we have a non-empty linebuffer, then return the first + # line from it. (Note that the last element of linebuffer may + # not be a complete line; so let _read() deal with it.) + if self.linebuffer and len(self.linebuffer) > 1: + line = self.linebuffer.pop(0) + self._rewind_numchars += len(line) + return line + + readsize = size or 72 + chars = "" + + # If there's a remaining incomplete line in the buffer, add it. + if self.linebuffer: + chars += self.linebuffer.pop() + self.linebuffer = None + + while True: + startpos = self.stream.tell() - len(self.bytebuffer) + new_chars = self._read(readsize) + + # If we're at a '\r', then read one extra character, since + # it might be a '\n', to get the proper line ending. + if new_chars and new_chars.endswith("\r"): + new_chars += self._read(1) + + chars += new_chars + lines = chars.splitlines(True) + if len(lines) > 1: + line = lines[0] + self.linebuffer = lines[1:] + self._rewind_numchars = len(new_chars) - (len(chars) - len(line)) + self._rewind_checkpoint = startpos + break + elif len(lines) == 1: + line0withend = lines[0] + line0withoutend = lines[0].splitlines(False)[0] + if line0withend != line0withoutend: # complete line + line = line0withend + break + + if not new_chars or size is not None: + line = chars + break + + # Read successively larger blocks of text. + if readsize < 8000: + readsize *= 2 + + return line + + def readlines(self, sizehint=None, keepends=True): + """ + Read this file's contents, decode them using this reader's + encoding, and return it as a list of unicode lines. + + :rtype: list(unicode) + :param sizehint: Ignored. + :param keepends: If false, then strip newlines. + """ + return self.read().splitlines(keepends) + + def next(self): + """Return the next decoded line from the underlying stream.""" + line = self.readline() + if line: + return line + else: + raise StopIteration + + def __next__(self): + return self.next() + + def __iter__(self): + """Return self""" + return self + + def __del__(self): + # let garbage collector deal with still opened streams + if not self.closed: + self.close() + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def xreadlines(self): + """Return self""" + return self + + # ///////////////////////////////////////////////////////////////// + # Pass-through methods & properties + # ///////////////////////////////////////////////////////////////// + + @property + def closed(self): + """True if the underlying stream is closed.""" + return self.stream.closed + + @property + def name(self): + """The name of the underlying stream.""" + return self.stream.name + + @property + def mode(self): + """The mode of the underlying stream.""" + return self.stream.mode + + def close(self): + """ + Close the underlying stream. + """ + self.stream.close() + + # ///////////////////////////////////////////////////////////////// + # Seek and tell + # ///////////////////////////////////////////////////////////////// + + def seek(self, offset, whence=0): + """ + Move the stream to a new file position. If the reader is + maintaining any buffers, then they will be cleared. + + :param offset: A byte count offset. + :param whence: If 0, then the offset is from the start of the file + (offset should be positive), if 1, then the offset is from the + current position (offset may be positive or negative); and if 2, + then the offset is from the end of the file (offset should + typically be negative). + """ + if whence == 1: + raise ValueError( + "Relative seek is not supported for " + "SeekableUnicodeStreamReader -- consider " + "using char_seek_forward() instead." + ) + self.stream.seek(offset, whence) + self.linebuffer = None + self.bytebuffer = b"" + self._rewind_numchars = None + self._rewind_checkpoint = self.stream.tell() + + def char_seek_forward(self, offset): + """ + Move the read pointer forward by ``offset`` characters. + """ + if offset < 0: + raise ValueError("Negative offsets are not supported") + # Clear all buffers. + self.seek(self.tell()) + # Perform the seek operation. + self._char_seek_forward(offset) + + def _char_seek_forward(self, offset, est_bytes=None): + """ + Move the file position forward by ``offset`` characters, + ignoring all buffers. + + :param est_bytes: A hint, giving an estimate of the number of + bytes that will be needed to move forward by ``offset`` chars. + Defaults to ``offset``. + """ + if est_bytes is None: + est_bytes = offset + bytes = b"" + + while True: + # Read in a block of bytes. + newbytes = self.stream.read(est_bytes - len(bytes)) + bytes += newbytes + + # Decode the bytes to characters. + chars, bytes_decoded = self._incr_decode(bytes) + + # If we got the right number of characters, then seek + # backwards over any truncated characters, and return. + if len(chars) == offset: + self.stream.seek(-len(bytes) + bytes_decoded, 1) + return + + # If we went too far, then we can back-up until we get it + # right, using the bytes we've already read. + if len(chars) > offset: + while len(chars) > offset: + # Assume at least one byte/char. + est_bytes += offset - len(chars) + chars, bytes_decoded = self._incr_decode(bytes[:est_bytes]) + self.stream.seek(-len(bytes) + bytes_decoded, 1) + return + + # Otherwise, we haven't read enough bytes yet; loop again. + est_bytes += offset - len(chars) + + def tell(self): + """ + Return the current file position on the underlying byte + stream. If this reader is maintaining any buffers, then the + returned file position will be the position of the beginning + of those buffers. + """ + # If nothing's buffered, then just return our current filepos: + if self.linebuffer is None: + return self.stream.tell() - len(self.bytebuffer) + + # Otherwise, we'll need to backtrack the filepos until we + # reach the beginning of the buffer. + + # Store our original file position, so we can return here. + orig_filepos = self.stream.tell() + + # Calculate an estimate of where we think the newline is. + bytes_read = (orig_filepos - len(self.bytebuffer)) - self._rewind_checkpoint + buf_size = sum(len(line) for line in self.linebuffer) + est_bytes = int( + bytes_read * self._rewind_numchars / (self._rewind_numchars + buf_size) + ) + + self.stream.seek(self._rewind_checkpoint) + self._char_seek_forward(self._rewind_numchars, est_bytes) + filepos = self.stream.tell() + + # Sanity check + if self.DEBUG: + self.stream.seek(filepos) + check1 = self._incr_decode(self.stream.read(50))[0] + check2 = "".join(self.linebuffer) + assert check1.startswith(check2) or check2.startswith(check1) + + # Return to our original filepos (so we don't have to throw + # out our buffer.) + self.stream.seek(orig_filepos) + + # Return the calculated filepos + return filepos + + # ///////////////////////////////////////////////////////////////// + # Helper methods + # ///////////////////////////////////////////////////////////////// + + def _read(self, size=None): + """ + Read up to ``size`` bytes from the underlying stream, decode + them using this reader's encoding, and return the resulting + unicode string. ``linebuffer`` is not included in the result. + """ + if size == 0: + return "" + + # Skip past the byte order marker, if present. + if self._bom and self.stream.tell() == 0: + self.stream.read(self._bom) + + # Read the requested number of bytes. + if size is None: + new_bytes = self.stream.read() + else: + new_bytes = self.stream.read(size) + bytes = self.bytebuffer + new_bytes + + # Decode the bytes into unicode characters + chars, bytes_decoded = self._incr_decode(bytes) + + # If we got bytes but couldn't decode any, then read further. + if (size is not None) and (not chars) and (len(new_bytes) > 0): + while not chars: + new_bytes = self.stream.read(1) + if not new_bytes: + break # end of file. + bytes += new_bytes + chars, bytes_decoded = self._incr_decode(bytes) + + # Record any bytes we didn't consume. + self.bytebuffer = bytes[bytes_decoded:] + + # Return the result + return chars + + def _incr_decode(self, bytes): + """ + Decode the given byte string into a unicode string, using this + reader's encoding. If an exception is encountered that + appears to be caused by a truncation error, then just decode + the byte string without the bytes that cause the trunctaion + error. + + Return a tuple ``(chars, num_consumed)``, where ``chars`` is + the decoded unicode string, and ``num_consumed`` is the + number of bytes that were consumed. + """ + while True: + try: + return self.decode(bytes, "strict") + except UnicodeDecodeError as exc: + # If the exception occurs at the end of the string, + # then assume that it's a truncation error. + if exc.end == len(bytes): + return self.decode(bytes[: exc.start], self.errors) + + # Otherwise, if we're being strict, then raise it. + elif self.errors == "strict": + raise + + # If we're not strict, then re-process it with our + # errors setting. This *may* raise an exception. + else: + return self.decode(bytes, self.errors) + + _BOM_TABLE = { + "utf8": [(codecs.BOM_UTF8, None)], + "utf16": [(codecs.BOM_UTF16_LE, "utf16-le"), (codecs.BOM_UTF16_BE, "utf16-be")], + "utf16le": [(codecs.BOM_UTF16_LE, None)], + "utf16be": [(codecs.BOM_UTF16_BE, None)], + "utf32": [(codecs.BOM_UTF32_LE, "utf32-le"), (codecs.BOM_UTF32_BE, "utf32-be")], + "utf32le": [(codecs.BOM_UTF32_LE, None)], + "utf32be": [(codecs.BOM_UTF32_BE, None)], + } + + def _check_bom(self): + # Normalize our encoding name + enc = re.sub("[ -]", "", self.encoding.lower()) + + # Look up our encoding in the BOM table. + bom_info = self._BOM_TABLE.get(enc) + + if bom_info: + # Read a prefix, to check against the BOM(s) + bytes = self.stream.read(16) + self.stream.seek(0) + + # Check for each possible BOM. + for bom, new_encoding in bom_info: + if bytes.startswith(bom): + if new_encoding: + self.encoding = new_encoding + return len(bom) + + return None + + +__all__ = [ + "path", + "PathPointer", + "FileSystemPathPointer", + "BufferedGzipFile", + "GzipFileSystemPathPointer", + "GzipFileSystemPathPointer", + "find", + "retrieve", + "FORMATS", + "AUTO_FORMATS", + "load", + "show_cfg", + "clear_cache", + "LazyLoader", + "OpenOnDemandZipFile", + "GzipFileSystemPathPointer", + "SeekableUnicodeStreamReader", +] diff --git a/nltk/decorators.py b/nltk/decorators.py new file mode 100644 index 0000000..aa17110 --- /dev/null +++ b/nltk/decorators.py @@ -0,0 +1,251 @@ +""" +Decorator module by Michele Simionato +Copyright Michele Simionato, distributed under the terms of the BSD License (see below). +http://www.phyast.pitt.edu/~micheles/python/documentation.html + +Included in NLTK for its support of a nice memoization decorator. +""" + +__docformat__ = "restructuredtext en" + +## The basic trick is to generate the source code for the decorated function +## with the right signature and to evaluate it. +## Uncomment the statement 'print >> sys.stderr, func_src' in _decorator +## to understand what is going on. + +__all__ = ["decorator", "new_wrapper", "getinfo"] + +import sys + +# Hack to keep NLTK's "tokenize" module from colliding with the "tokenize" in +# the Python standard library. +OLD_SYS_PATH = sys.path[:] +sys.path = [p for p in sys.path if p and "nltk" not in str(p)] +import inspect + +sys.path = OLD_SYS_PATH + + +def __legacysignature(signature): + """ + For retrocompatibility reasons, we don't use a standard Signature. + Instead, we use the string generated by this method. + Basically, from a Signature we create a string and remove the default values. + """ + listsignature = str(signature)[1:-1].split(",") + for counter, param in enumerate(listsignature): + if param.count("=") > 0: + listsignature[counter] = param[0 : param.index("=")].strip() + else: + listsignature[counter] = param.strip() + return ", ".join(listsignature) + + +def getinfo(func): + """ + Returns an info dictionary containing: + - name (the name of the function : str) + - argnames (the names of the arguments : list) + - defaults (the values of the default arguments : tuple) + - signature (the signature : str) + - fullsignature (the full signature : Signature) + - doc (the docstring : str) + - module (the module name : str) + - dict (the function __dict__ : str) + + >>> def f(self, x=1, y=2, *args, **kw): pass + + >>> info = getinfo(f) + + >>> info["name"] + 'f' + >>> info["argnames"] + ['self', 'x', 'y', 'args', 'kw'] + + >>> info["defaults"] + (1, 2) + + >>> info["signature"] + 'self, x, y, *args, **kw' + + >>> info["fullsignature"] + + """ + assert inspect.ismethod(func) or inspect.isfunction(func) + argspec = inspect.getfullargspec(func) + regargs, varargs, varkwargs = argspec[:3] + argnames = list(regargs) + if varargs: + argnames.append(varargs) + if varkwargs: + argnames.append(varkwargs) + fullsignature = inspect.signature(func) + # Convert Signature to str + signature = __legacysignature(fullsignature) + + # pypy compatibility + if hasattr(func, "__closure__"): + _closure = func.__closure__ + _globals = func.__globals__ + else: + _closure = func.func_closure + _globals = func.func_globals + + return dict( + name=func.__name__, + argnames=argnames, + signature=signature, + fullsignature=fullsignature, + defaults=func.__defaults__, + doc=func.__doc__, + module=func.__module__, + dict=func.__dict__, + globals=_globals, + closure=_closure, + ) + + +def update_wrapper(wrapper, model, infodict=None): + "akin to functools.update_wrapper" + infodict = infodict or getinfo(model) + wrapper.__name__ = infodict["name"] + wrapper.__doc__ = infodict["doc"] + wrapper.__module__ = infodict["module"] + wrapper.__dict__.update(infodict["dict"]) + wrapper.__defaults__ = infodict["defaults"] + wrapper.undecorated = model + return wrapper + + +def new_wrapper(wrapper, model): + """ + An improvement over functools.update_wrapper. The wrapper is a generic + callable object. It works by generating a copy of the wrapper with the + right signature and by updating the copy, not the original. + Moreovoer, 'model' can be a dictionary with keys 'name', 'doc', 'module', + 'dict', 'defaults'. + """ + if isinstance(model, dict): + infodict = model + else: # assume model is a function + infodict = getinfo(model) + assert ( + "_wrapper_" not in infodict["argnames"] + ), '"_wrapper_" is a reserved argument name!' + src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict + funcopy = eval(src, dict(_wrapper_=wrapper)) + return update_wrapper(funcopy, model, infodict) + + +# helper used in decorator_factory +def __call__(self, func): + return new_wrapper(lambda *a, **k: self.call(func, *a, **k), func) + + +def decorator_factory(cls): + """ + Take a class with a ``.caller`` method and return a callable decorator + object. It works by adding a suitable __call__ method to the class; + it raises a TypeError if the class already has a nontrivial __call__ + method. + """ + attrs = set(dir(cls)) + if "__call__" in attrs: + raise TypeError( + "You cannot decorate a class with a nontrivial " "__call__ method" + ) + if "call" not in attrs: + raise TypeError("You cannot decorate a class without a " ".call method") + cls.__call__ = __call__ + return cls + + +def decorator(caller): + """ + General purpose decorator factory: takes a caller function as + input and returns a decorator with the same attributes. + A caller function is any function like this:: + + def caller(func, *args, **kw): + # do something + return func(*args, **kw) + + Here is an example of usage: + + >>> @decorator + ... def chatty(f, *args, **kw): + ... print("Calling %r" % f.__name__) + ... return f(*args, **kw) + + >>> chatty.__name__ + 'chatty' + + >>> @chatty + ... def f(): pass + ... + >>> f() + Calling 'f' + + decorator can also take in input a class with a .caller method; in this + case it converts the class into a factory of callable decorator objects. + See the documentation for an example. + """ + if inspect.isclass(caller): + return decorator_factory(caller) + + def _decorator(func): # the real meat is here + infodict = getinfo(func) + argnames = infodict["argnames"] + assert not ( + "_call_" in argnames or "_func_" in argnames + ), "You cannot use _call_ or _func_ as argument names!" + src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict + # import sys; print >> sys.stderr, src # for debugging purposes + dec_func = eval(src, dict(_func_=func, _call_=caller)) + return update_wrapper(dec_func, func, infodict) + + return update_wrapper(_decorator, caller) + + +def getattr_(obj, name, default_thunk): + "Similar to .setdefault in dictionaries." + try: + return getattr(obj, name) + except AttributeError: + default = default_thunk() + setattr(obj, name, default) + return default + + +@decorator +def memoize(func, *args): + dic = getattr_(func, "memoize_dic", dict) + # memoize_dic is created at the first call + if args in dic: + return dic[args] + result = func(*args) + dic[args] = result + return result + + +########################## LEGALESE ############################### + +## Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## Redistributions in bytecode form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. + +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +## DAMAGE. diff --git a/nltk/downloader.py b/nltk/downloader.py new file mode 100644 index 0000000..3e822c8 --- /dev/null +++ b/nltk/downloader.py @@ -0,0 +1,3031 @@ +# Natural Language Toolkit: Corpus & Model Downloader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +The NLTK corpus and module downloader. This module defines several +interfaces which can be used to download corpora, models, and other +data packages that can be used with NLTK. + +Downloading Packages +==================== +If called with no arguments, ``download()`` will display an interactive +interface which can be used to download and install new packages. +If Tkinter is available, then a graphical interface will be shown, +otherwise a simple text interface will be provided. + +Individual packages can be downloaded by calling the ``download()`` +function with a single argument, giving the package identifier for the +package that should be downloaded: + + >>> download('treebank') # doctest: +SKIP + [nltk_data] Downloading package 'treebank'... + [nltk_data] Unzipping corpora/treebank.zip. + +NLTK also provides a number of \"package collections\", consisting of +a group of related packages. To download all packages in a +colleciton, simply call ``download()`` with the collection's +identifier: + + >>> download('all-corpora') # doctest: +SKIP + [nltk_data] Downloading package 'abc'... + [nltk_data] Unzipping corpora/abc.zip. + [nltk_data] Downloading package 'alpino'... + [nltk_data] Unzipping corpora/alpino.zip. + ... + [nltk_data] Downloading package 'words'... + [nltk_data] Unzipping corpora/words.zip. + +Download Directory +================== +By default, packages are installed in either a system-wide directory +(if Python has sufficient access to write to it); or in the current +user's home directory. However, the ``download_dir`` argument may be +used to specify a different installation target, if desired. + +See ``Downloader.default_download_dir()`` for more a detailed +description of how the default download directory is chosen. + +NLTK Download Server +==================== +Before downloading any packages, the corpus and module downloader +contacts the NLTK download server, to retrieve an index file +describing the available packages. By default, this index file is +loaded from ``https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml``. +If necessary, it is possible to create a new ``Downloader`` object, +specifying a different URL for the package index file. + +Usage:: + + python nltk/downloader.py [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS + +or:: + + python -m nltk.downloader [-d DATADIR] [-q] [-f] [-k] PACKAGE_IDS +""" + +# ---------------------------------------------------------------------- + +""" + + 0 1 2 3 +[label][----][label][----] +[column ][column ] + +Notes +===== +Handling data files.. Some questions: + +* Should the data files be kept zipped or unzipped? I say zipped. + +* Should the data files be kept in svn at all? Advantages: history; + automatic version numbers; 'svn up' could be used rather than the + downloader to update the corpora. Disadvantages: they're big, + which makes working from svn a bit of a pain. And we're planning + to potentially make them much bigger. I don't think we want + people to have to download 400MB corpora just to use nltk from svn. + +* Compromise: keep the data files in trunk/data rather than in + trunk/nltk. That way you can check them out in svn if you want + to; but you don't need to, and you can use the downloader instead. + +* Also: keep models in mind. When we change the code, we'd + potentially like the models to get updated. This could require a + little thought. + +* So.. let's assume we have a trunk/data directory, containing a bunch + of packages. The packages should be kept as zip files, because we + really shouldn't be editing them much (well -- we may edit models + more, but they tend to be binary-ish files anyway, where diffs + aren't that helpful). So we'll have trunk/data, with a bunch of + files like abc.zip and treebank.zip and propbank.zip. For each + package we could also have eg treebank.xml and propbank.xml, + describing the contents of the package (name, copyright, license, + etc). Collections would also have .xml files. Finally, we would + pull all these together to form a single index.xml file. Some + directory structure wouldn't hurt. So how about:: + + /trunk/data/ ....................... root of data svn + index.xml ........................ main index file + src/ ............................. python scripts + packages/ ........................ dir for packages + corpora/ ....................... zip & xml files for corpora + grammars/ ...................... zip & xml files for grammars + taggers/ ....................... zip & xml files for taggers + tokenizers/ .................... zip & xml files for tokenizers + etc. + collections/ ..................... xml files for collections + + Where the root (/trunk/data) would contain a makefile; and src/ + would contain a script to update the info.xml file. It could also + contain scripts to rebuild some of the various model files. The + script that builds index.xml should probably check that each zip + file expands entirely into a single subdir, whose name matches the + package's uid. + +Changes I need to make: + - in index: change "size" to "filesize" or "compressed-size" + - in index: add "unzipped-size" + - when checking status: check both compressed & uncompressed size. + uncompressed size is important to make sure we detect a problem + if something got partially unzipped. define new status values + to differentiate stale vs corrupt vs corruptly-uncompressed?? + (we shouldn't need to re-download the file if the zip file is ok + but it didn't get uncompressed fully.) + - add other fields to the index: author, license, copyright, contact, + etc. + +the current grammars/ package would become a single new package (eg +toy-grammars or book-grammars). + +xml file should have: + - authorship info + - license info + - copyright info + - contact info + - info about what type of data/annotation it contains? + - recommended corpus reader? + +collections can contain other collections. they can also contain +multiple package types (corpora & models). Have a single 'basics' +package that includes everything we talk about in the book? + +n.b.: there will have to be a fallback to the punkt tokenizer, in case +they didn't download that model. + +default: unzip or not? + +""" +import functools +import itertools +import os +import subprocess +import sys +import textwrap +import threading +import time +import warnings +import zipfile +from hashlib import md5, sha256 +from urllib.error import HTTPError, URLError +from xml.etree import ElementTree + +from defusedxml.ElementTree import parse as safe_parse + +import nltk +from nltk.data import _check_decompression_bomb +from nltk.pathsec import ZipFile +from nltk.pathsec import open as pathsec_open +from nltk.pathsec import urlopen, validate_path + +# urllib2 = nltk.internals.import_from_stdlib('urllib2') + + +###################################################################### +# Directory entry objects (from the data server's index file) +###################################################################### + + +class Package: + """ + A directory entry for a downloadable package. These entries are + extracted from the XML index file that is downloaded by + ``Downloader``. Each package consists of a single file; but if + that file is a zip file, then it can be automatically decompressed + when the package is installed. + """ + + def __init__( + self, + id, + url, + name=None, + subdir="", + size=None, + unzipped_size=None, + checksum=None, + svn_revision=None, + copyright="Unknown", + contact="Unknown", + license="Unknown", + author="Unknown", + unzip=True, + **kw, + ): + self.id = id + """A unique identifier for this package.""" + + self.name = name or id + """A string name for this package.""" + + # Validate subdir to prevent path traversal from malicious XML index + if os.path.isabs(subdir) or ".." in subdir.replace("\\", "/").split("/"): + raise ValueError( + f"Invalid package subdir {subdir!r}: must be a relative path " + f"without parent directory references" + ) + self.subdir = subdir + """The subdirectory where this package should be installed. + E.g., ``'corpora'`` or ``'taggers'``.""" + + # Validate id to prevent path traversal + if os.sep in id or "/" in id or "\\" in id or ".." in id: + raise ValueError( + f"Invalid package id {id!r}: must not contain path separators" + ) + + self.url = url + """A URL that can be used to download this package's file.""" + + self.size = int(size) + """The filesize (in bytes) of the package file.""" + + self.unzipped_size = int(unzipped_size) + """The total filesize of the files contained in the package's + zipfile.""" + + self.checksum = checksum + """The MD-5 checksum of the package file.""" + + self.svn_revision = svn_revision + """A subversion revision number for this package.""" + + self.copyright = copyright + """Copyright holder for this package.""" + + self.contact = contact + """Name & email of the person who should be contacted with + questions about this package.""" + + self.license = license + """License information for this package.""" + + self.author = author + """Author of this package.""" + + ext = os.path.splitext(url.split("/")[-1])[1] + + self.unzip = bool(int(unzip)) # '0' or '1' + """A flag indicating whether this corpus should be unzipped by + default.""" + + # Include any other attributes provided by the XML file. + self.__dict__.update(kw) + self.filename = os.path.join(subdir, id + ext) # Neutralize kw attack + """The filename that should be used for this package's file. It + is formed by joining ``self.subdir`` with ``self.id``, and + using the same extension as ``url``.""" + + @staticmethod + def fromxml(xml): + if isinstance(xml, str): + xml = ElementTree.parse(xml) + for key in xml.attrib: + xml.attrib[key] = str(xml.attrib[key]) + return Package(**xml.attrib) + + def __lt__(self, other): + return self.id < other.id + + def __repr__(self): + return "" % self.id + + +class Collection: + """ + A directory entry for a collection of downloadable packages. + These entries are extracted from the XML index file that is + downloaded by ``Downloader``. + """ + + def __init__(self, id, children, name=None, **kw): + self.id = id + """A unique identifier for this collection.""" + + self.name = name or id + """A string name for this collection.""" + + self.children = children + """A list of the ``Collections`` or ``Packages`` directly + contained by this collection.""" + + self.packages = None + """A list of ``Packages`` contained by this collection or any + collections it recursively contains.""" + + # Include any other attributes provided by the XML file. + self.__dict__.update(kw) + + @staticmethod + def fromxml(xml): + if isinstance(xml, str): + xml = ElementTree.parse(xml) + for key in xml.attrib: + xml.attrib[key] = str(xml.attrib[key]) + children = [child.get("ref") for child in xml.findall("item")] + return Collection(children=children, **xml.attrib) + + def __lt__(self, other): + return self.id < other.id + + def __repr__(self): + return "" % self.id + + +###################################################################### +# Message Passing Objects +###################################################################### + + +class DownloaderMessage: + """A status message object, used by ``incr_download`` to + communicate its progress.""" + + +class StartCollectionMessage(DownloaderMessage): + """Data server has started working on a collection of packages.""" + + def __init__(self, collection): + self.collection = collection + + +class FinishCollectionMessage(DownloaderMessage): + """Data server has finished working on a collection of packages.""" + + def __init__(self, collection): + self.collection = collection + + +class StartPackageMessage(DownloaderMessage): + """Data server has started working on a package.""" + + def __init__(self, package): + self.package = package + + +class FinishPackageMessage(DownloaderMessage): + """Data server has finished working on a package.""" + + def __init__(self, package): + self.package = package + + +class StartDownloadMessage(DownloaderMessage): + """Data server has started downloading a package.""" + + def __init__(self, package): + self.package = package + + +class FinishDownloadMessage(DownloaderMessage): + """Data server has finished downloading a package.""" + + def __init__(self, package): + self.package = package + + +class StartUnzipMessage(DownloaderMessage): + """Data server has started unzipping a package.""" + + def __init__(self, package): + self.package = package + + +class FinishUnzipMessage(DownloaderMessage): + """Data server has finished unzipping a package.""" + + def __init__(self, package): + self.package = package + + +class UpToDateMessage(DownloaderMessage): + """The package download file is already up-to-date""" + + def __init__(self, package): + self.package = package + + +class StaleMessage(DownloaderMessage): + """The package download file is out-of-date or corrupt""" + + def __init__(self, package): + self.package = package + + +class ErrorMessage(DownloaderMessage): + """Data server encountered an error""" + + def __init__(self, package, message): + self.package = package + if isinstance(message, Exception): + self.message = str(message) + else: + self.message = message + + +class ProgressMessage(DownloaderMessage): + """Indicates how much progress the data server has made""" + + def __init__(self, progress): + self.progress = progress + + +class SelectDownloadDirMessage(DownloaderMessage): + """Indicates what download directory the data server is using""" + + def __init__(self, download_dir): + self.download_dir = download_dir + + +###################################################################### +# NLTK Data Server +###################################################################### + + +class Downloader: + """ + A class used to access the NLTK data server, which can be used to + download corpora and other data packages. + """ + + # ///////////////////////////////////////////////////////////////// + # Configuration + # ///////////////////////////////////////////////////////////////// + + INDEX_TIMEOUT = 60 * 60 # 1 hour + """The amount of time after which the cached copy of the data + server index will be considered 'stale,' and will be + re-downloaded.""" + + DEFAULT_URL = "https://raw.githubusercontent.com/nltk/nltk_data/gh-pages/index.xml" + """The default URL for the NLTK data server's index. An + alternative URL can be specified when creating a new + ``Downloader`` object.""" + + # ///////////////////////////////////////////////////////////////// + # Status Constants + # ///////////////////////////////////////////////////////////////// + + INSTALLED = "installed" + """A status string indicating that a package or collection is + installed and up-to-date.""" + NOT_INSTALLED = "not installed" + """A status string indicating that a package or collection is + not installed.""" + STALE = "out of date" + """A status string indicating that a package or collection is + corrupt or out-of-date.""" + PARTIAL = "partial" + """A status string indicating that a collection is partially + installed (i.e., only some of its packages are installed.)""" + + # ///////////////////////////////////////////////////////////////// + # Constructor + # ///////////////////////////////////////////////////////////////// + + def __init__(self, server_index_url=None, download_dir=None): + self._url = server_index_url or self.DEFAULT_URL + """The URL for the data server's index file.""" + + self._collections = {} + """Dictionary from collection identifier to ``Collection``""" + + self._packages = {} + """Dictionary from package identifier to ``Package``""" + + self._download_dir = download_dir + """The default directory to which packages will be downloaded.""" + + self._index = None + """The XML index file downloaded from the data server""" + + self._index_timestamp = None + """Time at which ``self._index`` was downloaded. If it is more + than ``INDEX_TIMEOUT`` seconds old, it will be re-downloaded.""" + + self._status_cache = {} + """Dictionary from package/collection identifier to status + string (``INSTALLED``, ``NOT_INSTALLED``, ``STALE``, or + ``PARTIAL``). Cache is used for packages only, not + collections.""" + + self._errors = None + """Flag for telling if all packages got successfully downloaded or not.""" + + # decide where we're going to save things to. + if self._download_dir is None: + self._download_dir = self.default_download_dir() + + # ///////////////////////////////////////////////////////////////// + # Information + # ///////////////////////////////////////////////////////////////// + + def list( + self, + download_dir=None, + show_packages=True, + show_collections=True, + header=True, + more_prompt=False, + skip_installed=False, + ): + lines = 0 # for more_prompt + if download_dir is None: + download_dir = self._download_dir + print("Using default data directory (%s)" % download_dir) + if header: + print("=" * (26 + len(self._url))) + print(" Data server index for <%s>" % self._url) + print("=" * (26 + len(self._url))) + lines += 3 # for more_prompt + stale = partial = False + + categories = [] + if show_packages: + categories.append("packages") + if show_collections: + categories.append("collections") + for category in categories: + print("%s:" % category.capitalize()) + lines += 1 # for more_prompt + for info in sorted(getattr(self, category)(), key=str): + status = self.status(info, download_dir) + if status == self.INSTALLED and skip_installed: + continue + if status == self.STALE: + stale = True + if status == self.PARTIAL: + partial = True + prefix = { + self.INSTALLED: "*", + self.STALE: "-", + self.PARTIAL: "P", + self.NOT_INSTALLED: " ", + }[status] + name = textwrap.fill( + "-" * 27 + (info.name or info.id), 75, subsequent_indent=27 * " " + )[27:] + print(" [{}] {} {}".format(prefix, info.id.ljust(20, "."), name)) + lines += len(name.split("\n")) # for more_prompt + if more_prompt and lines > 20: + user_input = input("Hit Enter to continue: ") + if user_input.lower() in ("x", "q"): + return + lines = 0 + print() + msg = "([*] marks installed packages" + if stale: + msg += "; [-] marks out-of-date or corrupt packages" + if partial: + msg += "; [P] marks partially installed collections" + print(textwrap.fill(msg + ")", subsequent_indent=" ", width=76)) + + def packages(self): + self._update_index() + return self._packages.values() + + def corpora(self): + self._update_index() + return [pkg for (id, pkg) in self._packages.items() if pkg.subdir == "corpora"] + + def models(self): + self._update_index() + return [pkg for (id, pkg) in self._packages.items() if pkg.subdir != "corpora"] + + def collections(self): + self._update_index() + return self._collections.values() + + # ///////////////////////////////////////////////////////////////// + # Downloading + # ///////////////////////////////////////////////////////////////// + + def _info_or_id(self, info_or_id): + if isinstance(info_or_id, str): + return self.info(info_or_id) + else: + return info_or_id + + # [xx] When during downloading is it 'safe' to abort? Only unsafe + # time is *during* an unzip -- we don't want to leave a + # partially-unzipped corpus in place because we wouldn't notice + # it. But if we had the exact total size of the unzipped corpus, + # then that would be fine. Then we could abort anytime we want! + # So this is really what we should do. That way the threaded + # downloader in the gui can just kill the download thread anytime + # it wants. + + def incr_download(self, info_or_id, download_dir=None, force=False): + # If they didn't specify a download_dir, then use the default one. + if download_dir is None: + download_dir = self._download_dir + yield SelectDownloadDirMessage(download_dir) + + # If they gave us a list of ids, then download each one. + if isinstance(info_or_id, (list, tuple)): + yield from self._download_list(info_or_id, download_dir, force) + return + + # Look up the requested collection or package. + try: + info = self._info_or_id(info_or_id) + except (OSError, ValueError) as e: + yield ErrorMessage(None, f"Error loading {info_or_id}: {e}") + return + + # Handle collections. + if isinstance(info, Collection): + yield StartCollectionMessage(info) + yield from self.incr_download(info.children, download_dir, force) + yield FinishCollectionMessage(info) + + # Handle Packages (delegate to a helper function). + else: + yield from self._download_package(info, download_dir, force) + + def _num_packages(self, item): + if isinstance(item, Package): + return 1 + else: + return len(item.packages) + + def _download_list(self, items, download_dir, force): + # Look up the requested items. + for i in range(len(items)): + try: + items[i] = self._info_or_id(items[i]) + except (OSError, ValueError) as e: + yield ErrorMessage(items[i], e) + return + + # Download each item, re-scaling their progress. + num_packages = sum(self._num_packages(item) for item in items) + progress = 0 + for i, item in enumerate(items): + if isinstance(item, Package): + delta = 1.0 / num_packages + else: + delta = len(item.packages) / num_packages + for msg in self.incr_download(item, download_dir, force): + if isinstance(msg, ProgressMessage): + yield ProgressMessage(progress + msg.progress * delta) + else: + yield msg + + progress += 100 * delta + + def _download_package(self, info, download_dir, force): + yield StartPackageMessage(info) + yield ProgressMessage(0) + + filepath = os.path.join(download_dir, info.filename) + tmp_filepath = filepath + ".tmp" + lock_filepath = filepath + ".lock" + + # Defense-in-depth: verify the write target stays within download_dir. + # + # Two layers are required: + # + # (1) Lexical containment blocks "../" traversal coming from package + # metadata. It is intentionally lexical (normpath/abspath, not + # realpath) because the target leaf may not exist yet and on + # Windows realpath of a non-existent path can be unstable + # (short-name vs long-name, e.g. RUNNER~1 vs runneradmin). + # + # (2) Symlink containment (CWE-59, "link following"). A purely lexical + # check is bypassed by a symlink that already exists *inside* + # download_dir and points outside it: the target is lexically + # contained, but open()/os.replace() follow the link and write + # outside. We therefore also resolve the deepest *existing* + # ancestor of the real write targets (the ".tmp" file open()s, and + # the final filepath os.replace()s onto) and require it to stay + # within download_dir. Only existing components are resolved, so + # the not-yet-existing-leaf concern that motivates (1) does not + # apply here. + def _within(child, root): + try: + return os.path.commonpath([root, child]) == root + except ValueError: + return False + + def _symlink_contained(path): + # Walk up to the deepest *lexically* existing ancestor. os.path.lexists + # (not os.path.exists) is required so a planted symlink -- including a + # dangling one -- stops the walk and gets resolved, rather than being + # treated as a non-existent leaf and skipped. + ancestor = os.path.abspath(path) + while not os.path.lexists(ancestor): + parent = os.path.dirname(ancestor) + if parent == ancestor: + break + ancestor = parent + # Decide from the *lexical* ancestor whether the walk stopped at or + # above download_dir (e.g. download_dir itself does not exist yet, so + # the walk climbed past it). In that case no symlink *inside* + # download_dir could have redirected the path -- lexical containment + # already governs that case. This must use the lexical ancestor, not + # the resolved one: a symlink that exists *inside* download_dir and + # points to a parent of download_dir would otherwise resolve to an + # ancestor of download_dir and wrongly take this early-return, even + # though following it escapes the download directory. + lex_ancestor = os.path.normcase(ancestor) + lex_download = os.path.normcase( + os.path.abspath(os.path.normpath(download_dir)) + ) + if _within(lex_download, lex_ancestor): + return True + # The deepest existing ancestor lives lexically below download_dir; + # resolve symlinks on it and require it to stay within download_dir. + real_ancestor = os.path.normcase(os.path.realpath(ancestor)) + real_download = os.path.normcase(os.path.realpath(download_dir)) + return _within(real_ancestor, real_download) + + safe_download = os.path.normcase( + os.path.abspath(os.path.normpath(download_dir)) + ) + safe_filepath = os.path.normcase(os.path.abspath(os.path.normpath(filepath))) + if not ( + _within(safe_filepath, safe_download) + and _symlink_contained(filepath) + and _symlink_contained(tmp_filepath) + ): + yield ErrorMessage( + info, + f"Path traversal blocked: package '{info.id}' attempted to " + f"write outside download directory (subdir='{info.subdir}')", + ) + return + + unzipdir = filepath[:-4] if filepath.endswith(".zip") else None + MAX_ZOMBIE_TIME = 60 + POLL_INTERVAL = 1 + + def _safe_remove(path): + try: + os.remove(path) + except FileNotFoundError: + pass + except OSError: + pass + + def _safe_rmtree(path): + # Never follow a symlinked directory while cleaning up: os.walk + # descends into a symlinked top regardless of ``followlinks``, so a + # pre-existing symlink at ``path`` (e.g. a planted unzip dir) would + # cause recursive deletion of the link's target *outside* the + # download directory (CWE-59 / CWE-22). Remove the link itself. + if os.path.islink(path): + _safe_remove(path) + return + if not os.path.isdir(path): + return + for root, dirs, files in os.walk(path, topdown=False): + for name in files: + try: + os.remove(os.path.join(root, name)) + except OSError: + pass + for name in dirs: + full = os.path.join(root, name) + # os.walk (followlinks=False) lists but does not descend + # symlinked subdirs; remove the link, never its target. + if os.path.islink(full): + _safe_remove(full) + continue + try: + os.rmdir(full) + except OSError: + pass + try: + os.rmdir(path) + except OSError: + pass + + def _touch_lock(): + try: + os.utime(lock_filepath, None) + except OSError: + pass + + def _lock_exists(): + return os.path.exists(lock_filepath) + + def _status_now(): + self._status_cache.pop(info.id, None) + return self.status(info, download_dir) + + def _installed_now(): + return _status_now() == self.INSTALLED + + os.makedirs(download_dir, exist_ok=True) + os.makedirs(os.path.join(download_dir, info.subdir), exist_ok=True) + + # Fast path before taking the lock. + # Do not return "up to date" while another process still holds the install lock. + if not force and _installed_now() and not _lock_exists(): + yield UpToDateMessage(info) + yield ProgressMessage(100) + yield FinishPackageMessage(info) + return + + # Acquire a package-wide install lock that covers download + unzip. + while True: + if not force and _installed_now() and not _lock_exists(): + yield UpToDateMessage(info) + yield ProgressMessage(100) + yield FinishPackageMessage(info) + return + + try: + fd = os.open(lock_filepath, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + os.close(fd) + break + except FileExistsError: + try: + age = time.time() - os.stat(lock_filepath).st_mtime + if age >= MAX_ZOMBIE_TIME: + _safe_remove(lock_filepath) + continue + except FileNotFoundError: + continue + time.sleep(POLL_INTERVAL) + + try: + # Recheck after lock acquisition in case another process completed first. + if not force and _installed_now(): + yield UpToDateMessage(info) + yield ProgressMessage(100) + yield FinishPackageMessage(info) + return + + status = _status_now() + + # Only the lock holder may clean stale state. + if status == self.STALE: + yield StaleMessage(info) + _safe_remove(tmp_filepath) + _safe_remove(filepath) + if unzipdir: + _safe_rmtree(unzipdir) + self._status_cache.pop(info.id, None) + + # Download if needed. + if force or not os.path.exists(filepath): + yield StartDownloadMessage(info) + yield ProgressMessage(5) + + try: + infile = urlopen(info.url) + with pathsec_open( + tmp_filepath, + "wb", + context="Downloader._download_package", + required_root=download_dir, + ) as outfile: + num_blocks = max(1, info.size / (1024 * 16)) + for block in itertools.count(): + s = infile.read(1024 * 16) + if not s: + break + outfile.write(s) + if block % 2 == 0: + _touch_lock() + yield ProgressMessage( + min(80, 5 + 75 * (block / num_blocks)) + ) + infile.close() + # --- CVE-2026-12261 fix (integrity before commit) --- + # Validate size and sha256 on the temp file BEFORE + # replacing the destination. This closes the window + # where a tampered archive could be extracted while the + # integrity check was still deferred to status logic. + tmp_size = os.path.getsize(tmp_filepath) + if tmp_size != int(info.size): + _safe_remove(tmp_filepath) + yield ErrorMessage( + info, + f"Integrity check failed for {info.id!r}: " + f"size mismatch (got {tmp_size}, expected {info.size})", + ) + return + + sha256_checksum = getattr(info, "sha256_checksum", None) + + if sha256_checksum: + if sha256_hexdigest(tmp_filepath) != sha256_checksum: + _safe_remove(tmp_filepath) + yield ErrorMessage( + info, + f"Integrity check failed for {info.id!r}: sha256 mismatch", + ) + return + else: + md5_checksum = getattr(info, "checksum", None) + + if md5_checksum and md5_hexdigest(tmp_filepath) != md5_checksum: + _safe_remove(tmp_filepath) + yield ErrorMessage( + info, + f"Integrity check failed for {info.id!r}: md5 mismatch", + ) + return + # --------------------------------------------------- + + validate_path( + filepath, + context="Downloader._download_package", + required_root=download_dir, + ) + os.replace(tmp_filepath, filepath) + self._status_cache.pop(info.id, None) + except OSError as e: + _safe_remove(tmp_filepath) + yield ErrorMessage( + info, + "Error downloading %r from <%s>:" + "\n %s" % (info.id, info.url, e), + ) + return + + yield FinishDownloadMessage(info) + yield ProgressMessage(80) + + # Unzip while still holding the same install lock. + if info.filename.endswith(".zip"): + zipdir = os.path.join(download_dir, info.subdir) + if info.unzip or os.path.exists(os.path.join(zipdir, info.id)): + yield StartUnzipMessage(info) + # --- CVE-2026-12261 fix (member ownership enforcement) --- + # Pass info.id so _unzip_iter can reject any archive member + # whose top-level path component is not the owning package. + # zipdir itself stays as download_dir/subdir/ to preserve + # the layout expected by _pkg_status and NLTK data loaders. + for msg in _unzip_iter( + filepath, zipdir, verbose=False, expected_root=info.id + ): + _touch_lock() + msg.package = info + yield msg + if isinstance(msg, ErrorMessage): + return + yield FinishUnzipMessage(info) + + # Final verification: followers should only ever observe INSTALLED. + if not _installed_now(): + yield ErrorMessage( + info, + f"Package {info.id!r} did not reach installed state " + f"(final status: {_status_now()})", + ) + return + + yield FinishPackageMessage(info) + + finally: + _safe_remove(tmp_filepath) + _safe_remove(lock_filepath) + + def download( + self, + info_or_id=None, + download_dir=None, + quiet=False, + force=False, + prefix="[nltk_data] ", + halt_on_error=True, + raise_on_error=False, + print_error_to=sys.stderr, + hf=False, + ): + # Delegate to HuggingFace downloader when hf=True. + if hf and info_or_id is not None: + from nltk.huggingface.dataset import download as hf_download + + return hf_download(info_or_id, quiet=quiet) + + print_to = functools.partial(print, file=print_error_to) + # If no info or id is given, then use the interactive shell. + if info_or_id is None: + # [xx] hmm -- changing self._download_dir here seems like + # the wrong thing to do. Maybe the _interactive_download + # function should make a new copy of self to use? + if download_dir is not None: + self._download_dir = download_dir + self._interactive_download() + return True + + else: + # Define a helper function for displaying output: + def show(s, prefix2=""): + print_to( + textwrap.fill( + s, + initial_indent=prefix + prefix2, + subsequent_indent=prefix + prefix2 + " " * 4, + ) + ) + + for msg in self.incr_download(info_or_id, download_dir, force): + # Error messages + if isinstance(msg, ErrorMessage): + show(msg.message) + if raise_on_error: + raise ValueError(msg.message) + if halt_on_error: + return False + self._errors = True + if not quiet: + print_to("Error installing package. Retry? [n/y/e]") + choice = input().strip() + if choice in ["y", "Y"]: + if not self.download( + msg.package.id, + download_dir, + quiet, + force, + prefix, + halt_on_error, + raise_on_error, + ): + return False + elif choice in ["e", "E"]: + return False + + # All other messages + if not quiet: + # Collection downloading messages: + if isinstance(msg, StartCollectionMessage): + show("Downloading collection %r" % msg.collection.id) + prefix += " | " + print_to(prefix) + elif isinstance(msg, FinishCollectionMessage): + print_to(prefix) + prefix = prefix[:-4] + if self._errors: + show( + "Downloaded collection %r with errors" + % msg.collection.id + ) + else: + show("Done downloading collection %s" % msg.collection.id) + + # Package downloading messages: + elif isinstance(msg, StartPackageMessage): + show( + "Downloading package %s to %s..." + % (msg.package.id, download_dir) + ) + elif isinstance(msg, UpToDateMessage): + show("Package %s is already up-to-date!" % msg.package.id, " ") + # elif isinstance(msg, StaleMessage): + # show('Package %s is out-of-date or corrupt' % + # msg.package.id, ' ') + elif isinstance(msg, StartUnzipMessage): + show("Unzipping %s." % msg.package.filename, " ") + + # Data directory message: + elif isinstance(msg, SelectDownloadDirMessage): + download_dir = msg.download_dir + return True + + def is_stale(self, info_or_id, download_dir=None): + return self.status(info_or_id, download_dir) == self.STALE + + def is_installed(self, info_or_id, download_dir=None): + return self.status(info_or_id, download_dir) == self.INSTALLED + + def clear_status_cache(self, id=None): + if id is None: + self._status_cache.clear() + else: + self._status_cache.pop(id, None) + + def status(self, info_or_id, download_dir=None): + """ + Return a constant describing the status of the given package + or collection. Status can be one of ``INSTALLED``, + ``NOT_INSTALLED``, ``STALE``, or ``PARTIAL``. + """ + if download_dir is None: + download_dir = self._download_dir + info = self._info_or_id(info_or_id) + + # Handle collections: + if isinstance(info, Collection): + pkg_status = [self.status(pkg.id) for pkg in info.packages] + if self.STALE in pkg_status: + return self.STALE + elif self.PARTIAL in pkg_status: + return self.PARTIAL + elif self.INSTALLED in pkg_status and self.NOT_INSTALLED in pkg_status: + return self.PARTIAL + elif self.NOT_INSTALLED in pkg_status: + return self.NOT_INSTALLED + else: + return self.INSTALLED + + # Handle packages: + else: + filepath = os.path.join(download_dir, info.filename) + if download_dir != self._download_dir: + return self._pkg_status(info, filepath) + else: + if info.id not in self._status_cache: + self._status_cache[info.id] = self._pkg_status(info, filepath) + return self._status_cache[info.id] + + def _pkg_status(self, info, filepath): + if not os.path.exists(filepath): + return self.NOT_INSTALLED + + # Check if the file has the correct size. + try: + filestat = os.stat(filepath) + except OSError: + return self.NOT_INSTALLED + if filestat.st_size != int(info.size): + return self.STALE + + # Check if the file's checksum matches. + # Prefer sha256, but fall back to legacy md5 if sha256 is unavailable. + sha256_checksum = getattr(info, "sha256_checksum", None) + if sha256_checksum: + if sha256_hexdigest(filepath) != sha256_checksum: + return self.STALE + else: + if not getattr(self, "_warned_missing_sha256_checksum", False): + warnings.warn( + "NLTK downloader package metadata is missing SHA256 checksums; " + "falling back to legacy MD5 verification. Consider updating your " + "NLTK data index (or re-running the downloader) to get SHA256 checksums.", + RuntimeWarning, + stacklevel=2, + ) + self._warned_missing_sha256_checksum = True + + md5_checksum = getattr(info, "checksum", None) + if not md5_checksum: + return self.STALE + if md5_hexdigest(filepath) != md5_checksum: + return self.STALE + + # If it's a zipfile, and it's been at least partially + # unzipped, then check if it's been fully unzipped. + if filepath.endswith(".zip"): + unzipdir = filepath[:-4] + if not os.path.exists(unzipdir): + return self.NOT_INSTALLED if info.unzip else self.INSTALLED + if not os.path.isdir(unzipdir): + return self.STALE + + unzipped_size = sum( + os.stat(os.path.join(d, f)).st_size + for d, _, files in os.walk(unzipdir) + for f in files + ) + if unzipped_size != info.unzipped_size: + return self.STALE + + # Otherwise, everything looks good. + return self.INSTALLED + + def update(self, quiet=False, prefix="[nltk_data] "): + """ + Re-download any packages whose status is STALE. + """ + self.clear_status_cache() + for pkg in self.packages(): + if self.status(pkg) == self.STALE: + self.download(pkg, quiet=quiet, prefix=prefix) + + # ///////////////////////////////////////////////////////////////// + # Index + # ///////////////////////////////////////////////////////////////// + + def _update_index(self, url=None): + """A helper function that ensures that self._index is + up-to-date. If the index is older than self.INDEX_TIMEOUT, + then download it again.""" + # Check if the index is already up-to-date. If so, do nothing. + if not ( + self._index is None + or url is not None + or time.time() - self._index_timestamp > self.INDEX_TIMEOUT + ): + return + + # If a URL was specified, then update our URL. + self._url = url or self._url + + # Download the index file. + self._index = nltk.internals.ElementWrapper( + safe_parse(urlopen(self._url)).getroot() + ) + self._index_timestamp = time.time() + + # Build a dictionary of packages. + packages = [Package.fromxml(p) for p in self._index.findall("packages/package")] + self._packages = {p.id: p for p in packages} + + # Build a dictionary of collections. + collections = [ + Collection.fromxml(c) for c in self._index.findall("collections/collection") + ] + self._collections = {c.id: c for c in collections} + + # Replace identifiers with actual children in collection.children. + for collection in self._collections.values(): + for i, child_id in enumerate(collection.children): + if child_id in self._packages: + collection.children[i] = self._packages[child_id] + elif child_id in self._collections: + collection.children[i] = self._collections[child_id] + else: + print( + "removing collection member with no package: {}".format( + child_id + ) + ) + del collection.children[i] + + # Fill in collection.packages for each collection. + for collection in self._collections.values(): + packages = {} + queue = [collection] + for child in queue: + if isinstance(child, Collection): + queue.extend(child.children) + elif isinstance(child, Package): + packages[child.id] = child + else: + pass + collection.packages = packages.values() + + # Flush the status cache + self._status_cache.clear() + + def index(self): + """ + Return the XML index describing the packages available from + the data server. If necessary, this index will be downloaded + from the data server. + """ + self._update_index() + return self._index + + def info(self, id): + """Return the ``Package`` or ``Collection`` record for the + given item.""" + self._update_index() + if id in self._packages: + return self._packages[id] + if id in self._collections: + return self._collections[id] + raise ValueError("Package %r not found in index" % id) + + def xmlinfo(self, id): + """Return the XML info record for the given item""" + self._update_index() + for package in self._index.findall("packages/package"): + if package.get("id") == id: + return package + for collection in self._index.findall("collections/collection"): + if collection.get("id") == id: + return collection + raise ValueError("Package %r not found in index" % id) + + # ///////////////////////////////////////////////////////////////// + # URL & Data Directory + # ///////////////////////////////////////////////////////////////// + + def _get_url(self): + """The URL for the data server's index file.""" + return self._url + + def _set_url(self, url): + """ + Set a new URL for the data server. If we're unable to contact + the given url, then the original url is kept. + """ + original_url = self._url + try: + self._update_index(url) + except Exception: + self._url = original_url + raise + + url = property(_get_url, _set_url) + + def default_download_dir(self): + """ + Return the directory to which packages will be downloaded by + default. This value can be overridden using the constructor, + or on a case-by-case basis using the ``download_dir`` argument when + calling ``download()``. + + On Windows, the default download directory is + ``PYTHONHOME/lib/nltk``, where *PYTHONHOME* is the + directory containing Python, e.g. ``C:\\Python25``. + + On all other platforms, the default directory is the first of + the following which exists or which can be created with write + permission: ``/usr/share/nltk_data``, ``/usr/local/share/nltk_data``, + ``/usr/lib/nltk_data``, ``/usr/local/lib/nltk_data``, ``~/nltk_data``. + """ + # Check if we are on GAE where we cannot write into filesystem. + if "APPENGINE_RUNTIME" in os.environ: + return + + # Check if we have sufficient permissions to install in a + # variety of system-wide locations. + for nltkdir in nltk.data.path: + if os.path.exists(nltkdir) and nltk.internals.is_writable(nltkdir): + return nltkdir + + # On Windows, use %APPDATA% + if os.name == "nt" and "APPDATA" in os.environ: + homedir = os.environ["APPDATA"] + + # Otherwise, install in the user's home directory. + else: + homedir = os.path.expanduser("~/") + if homedir == "~/": + raise ValueError("Could not find a default download directory") + + # append "nltk_data" to the home directory + return os.path.join(homedir, "nltk_data") + + def _get_download_dir(self): + """ + The default directory to which packages will be downloaded. + This defaults to the value returned by ``default_download_dir()``. + To override this default on a case-by-case basis, use the + ``download_dir`` argument when calling ``download()``. + """ + return self._download_dir + + def _set_download_dir(self, download_dir): + self._download_dir = download_dir + # Clear the status cache. + self._status_cache.clear() + + download_dir = property(_get_download_dir, _set_download_dir) + + # ///////////////////////////////////////////////////////////////// + # Interactive Shell + # ///////////////////////////////////////////////////////////////// + + def _interactive_download(self): + # Only import tkinter if the user has indicated that they + # want to draw a UI. See issue #2949 for more info. + if ( + os.environ.get("NLTK_DOWNLOADER_FORCE_INTERACTIVE_SHELL", "false").lower() + == "true" + ): + DownloaderShell(self).run() + return + + # Try the GUI first; if that doesn't work, try the simple + # interactive shell. + try: + import tkinter + except ImportError: + DownloaderShell(self).run() + return + + try: + DownloaderGUI(self).mainloop() + except tkinter.TclError: + DownloaderShell(self).run() + + +class DownloaderShell: + def __init__(self, dataserver): + self._ds = dataserver + + def _simple_interactive_menu(self, *options): + print("-" * 75) + spc = (68 - sum(len(o) for o in options)) // (len(options) - 1) * " " + print(" " + spc.join(options)) + print("-" * 75) + + def run(self): + print("NLTK Downloader") + while True: + self._simple_interactive_menu( + "d) Download", + "l) List", + " u) Update", + "c) Config", + "h) Help", + "q) Quit", + ) + user_input = input("Downloader> ").strip() + if not user_input: + print() + continue + command = user_input.lower().split()[0] + args = user_input.split()[1:] + try: + if command == "l": + print() + self._ds.list(self._ds.download_dir, header=False, more_prompt=True) + elif command == "h": + self._simple_interactive_help() + elif command == "c": + self._simple_interactive_config() + elif command in ("q", "x"): + return + elif command == "d": + self._simple_interactive_download(args) + elif command == "u": + self._simple_interactive_update() + else: + print("Command %r unrecognized" % user_input) + except HTTPError as e: + print("Error reading from server: %s" % e) + except URLError as e: + print("Error connecting to server: %s" % e.reason) + # try checking if user_input is a package name, & + # downloading it? + print() + + def _simple_interactive_download(self, args): + if args: + for arg in args: + try: + self._ds.download(arg, prefix=" ") + except (OSError, ValueError) as e: + print(e) + else: + while True: + print() + print("Download which package (l=list; x=cancel)?") + user_input = input(" Identifier> ") + if user_input.lower() == "l": + self._ds.list( + self._ds.download_dir, + header=False, + more_prompt=True, + skip_installed=True, + ) + continue + elif user_input.lower() in ("x", "q", ""): + return + elif user_input: + for id in user_input.split(): + try: + self._ds.download(id, prefix=" ") + except (OSError, ValueError) as e: + print(e) + break + + def _simple_interactive_update(self): + while True: + stale_packages = [] + stale = partial = False + for info in sorted(getattr(self._ds, "packages")(), key=str): + if self._ds.status(info) == self._ds.STALE: + stale_packages.append((info.id, info.name)) + + print() + if stale_packages: + print("Will update following packages (o=ok; x=cancel)") + for pid, pname in stale_packages: + name = textwrap.fill( + "-" * 27 + (pname), 75, subsequent_indent=27 * " " + )[27:] + print(" [ ] {} {}".format(pid.ljust(20, "."), name)) + print() + + user_input = input(" Identifier> ") + if user_input.lower() == "o": + for pid, pname in stale_packages: + try: + self._ds.download(pid, prefix=" ") + except (OSError, ValueError) as e: + print(e) + break + elif user_input.lower() in ("x", "q", ""): + return + else: + print("Nothing to update.") + return + + def _simple_interactive_help(self): + print() + print("Commands:") + print( + " d) Download a package or collection u) Update out of date packages" + ) + print(" l) List packages & collections h) Help") + print(" c) View & Modify Configuration q) Quit") + + def _show_config(self): + print() + print("Data Server:") + print(" - URL: <%s>" % self._ds.url) + print(" - %d Package Collections Available" % len(self._ds.collections())) + print(" - %d Individual Packages Available" % len(self._ds.packages())) + print() + print("Local Machine:") + print(" - Data directory: %s" % self._ds.download_dir) + + def _simple_interactive_config(self): + self._show_config() + while True: + print() + self._simple_interactive_menu( + "s) Show Config", "u) Set Server URL", "d) Set Data Dir", "m) Main Menu" + ) + user_input = input("Config> ").strip().lower() + if user_input == "s": + self._show_config() + elif user_input == "d": + new_dl_dir = input(" New Directory> ").strip() + if new_dl_dir in ("", "x", "q", "X", "Q"): + print(" Cancelled!") + elif os.path.isdir(new_dl_dir): + self._ds.download_dir = new_dl_dir + else: + print("Directory %r not found! Create it first." % new_dl_dir) + elif user_input == "u": + new_url = input(" New URL> ").strip() + if new_url in ("", "x", "q", "X", "Q"): + print(" Cancelled!") + else: + if not new_url.startswith(("http://", "https://")): + new_url = "https://" + new_url + try: + self._ds.url = new_url + except Exception as e: + print(f"Error reading <{new_url!r}>:\n {e}") + elif user_input == "m": + break + + +class DownloaderGUI: + """ + Graphical interface for downloading packages from the NLTK data + server. + """ + + # ///////////////////////////////////////////////////////////////// + # Column Configuration + # ///////////////////////////////////////////////////////////////// + + COLUMNS = [ + "", + "Identifier", + "Name", + "Size", + "Status", + "Unzipped Size", + "Copyright", + "Contact", + "License", + "Author", + "Subdir", + "Checksum", + ] + """A list of the names of columns. This controls the order in + which the columns will appear. If this is edited, then + ``_package_to_columns()`` may need to be edited to match.""" + + COLUMN_WEIGHTS = {"": 0, "Name": 5, "Size": 0, "Status": 0} + """A dictionary specifying how columns should be resized when the + table is resized. Columns with weight 0 will not be resized at + all; and columns with high weight will be resized more. + Default weight (for columns not explicitly listed) is 1.""" + + COLUMN_WIDTHS = { + "": 1, + "Identifier": 20, + "Name": 45, + "Size": 10, + "Unzipped Size": 10, + "Status": 12, + } + """A dictionary specifying how wide each column should be, in + characters. The default width (for columns not explicitly + listed) is specified by ``DEFAULT_COLUMN_WIDTH``.""" + + DEFAULT_COLUMN_WIDTH = 30 + """The default width for columns that are not explicitly listed + in ``COLUMN_WIDTHS``.""" + + INITIAL_COLUMNS = ["", "Identifier", "Name", "Size", "Status"] + """The set of columns that should be displayed by default.""" + + # Perform a few import-time sanity checks to make sure that the + # column configuration variables are defined consistently: + for c in COLUMN_WEIGHTS: + assert c in COLUMNS + for c in COLUMN_WIDTHS: + assert c in COLUMNS + for c in INITIAL_COLUMNS: + assert c in COLUMNS + + # ///////////////////////////////////////////////////////////////// + # Color Configuration + # ///////////////////////////////////////////////////////////////// + + _BACKDROP_COLOR = ("#000", "#ccc") + + _ROW_COLOR = { + Downloader.INSTALLED: ("#afa", "#080"), + Downloader.PARTIAL: ("#ffa", "#880"), + Downloader.STALE: ("#faa", "#800"), + Downloader.NOT_INSTALLED: ("#fff", "#888"), + } + + _MARK_COLOR = ("#000", "#ccc") + + # _FRONT_TAB_COLOR = ('#ccf', '#008') + # _BACK_TAB_COLOR = ('#88a', '#448') + _FRONT_TAB_COLOR = ("#fff", "#45c") + _BACK_TAB_COLOR = ("#aaa", "#67a") + + _PROGRESS_COLOR = ("#f00", "#aaa") + + _TAB_FONT = "helvetica -16 bold" + + # ///////////////////////////////////////////////////////////////// + # Constructor + # ///////////////////////////////////////////////////////////////// + + def __init__(self, dataserver, use_threads=True): + # Only import tkinter if the user has indicated that they + # want to draw a UI. See issue #2949 for more info. + import tkinter + from tkinter.messagebox import showerror + + self._ds = dataserver + self._use_threads = use_threads + + # For the threaded downloader: + self._download_lock = threading.Lock() + self._download_msg_queue = [] + self._download_abort_queue = [] + self._downloading = False + + # For tkinter after callbacks: + self._afterid = {} + + # A message log. + self._log_messages = [] + self._log_indent = 0 + self._log("NLTK Downloader Started!") + + # Create the main window. + top = self.top = tkinter.Tk() + top.geometry("+50+50") + top.title("NLTK Downloader") + top.configure(background=self._BACKDROP_COLOR[1]) + + # Set up some bindings now, in case anything goes wrong. + top.bind("", self.destroy) + top.bind("", self.destroy) + self._destroyed = False + + self._column_vars = {} + + # Initialize the GUI. + self._init_widgets() + self._init_menu() + try: + self._fill_table() + except HTTPError as e: + showerror("Error reading from server", e) + except URLError as e: + showerror("Error connecting to server", e.reason) + + self._show_info() + self._select_columns() + self._table.select(0) + + # Make sure we get notified when we're destroyed, so we can + # cancel any download in progress. + self._table.bind("", self._destroy) + + def _log(self, msg): + self._log_messages.append( + "{} {}{}".format(time.ctime(), " | " * self._log_indent, msg) + ) + + # ///////////////////////////////////////////////////////////////// + # Internals + # ///////////////////////////////////////////////////////////////// + + def _init_widgets(self): + # Only import tkinter if the user has indicated that they + # want to draw a UI. See issue #2949 for more info. + import tkinter + + from nltk.draw.table import Table + + # Create the top-level frame structures + f1 = tkinter.Frame(self.top, relief="raised", border=2, padx=8, pady=0) + f1.pack(sid="top", expand=True, fill="both") + f1.grid_rowconfigure(2, weight=1) + f1.grid_columnconfigure(0, weight=1) + tkinter.Frame(f1, height=8).grid(column=0, row=0) # spacer + tabframe = tkinter.Frame(f1) + tabframe.grid(column=0, row=1, sticky="news") + tableframe = tkinter.Frame(f1) + tableframe.grid(column=0, row=2, sticky="news") + buttonframe = tkinter.Frame(f1) + buttonframe.grid(column=0, row=3, sticky="news") + tkinter.Frame(f1, height=8).grid(column=0, row=4) # spacer + infoframe = tkinter.Frame(f1) + infoframe.grid(column=0, row=5, sticky="news") + tkinter.Frame(f1, height=8).grid(column=0, row=6) # spacer + progressframe = tkinter.Frame( + self.top, padx=3, pady=3, background=self._BACKDROP_COLOR[1] + ) + progressframe.pack(side="bottom", fill="x") + self.top["border"] = 0 + self.top["highlightthickness"] = 0 + + # Create the tabs + self._tab_names = ["Collections", "Corpora", "Models", "All Packages"] + self._tabs = {} + for i, tab in enumerate(self._tab_names): + label = tkinter.Label(tabframe, text=tab, font=self._TAB_FONT) + label.pack(side="left", padx=((i + 1) % 2) * 10) + label.bind("", self._select_tab) + self._tabs[tab.lower()] = label + + # Create the table. + column_weights = [self.COLUMN_WEIGHTS.get(column, 1) for column in self.COLUMNS] + self._table = Table( + tableframe, + self.COLUMNS, + column_weights=column_weights, + highlightthickness=0, + listbox_height=16, + reprfunc=self._table_reprfunc, + ) + self._table.columnconfig(0, foreground=self._MARK_COLOR[0]) # marked + for i, column in enumerate(self.COLUMNS): + width = self.COLUMN_WIDTHS.get(column, self.DEFAULT_COLUMN_WIDTH) + self._table.columnconfig(i, width=width) + self._table.pack(expand=True, fill="both") + self._table.focus() + self._table.bind_to_listboxes("", self._download) + self._table.bind("", self._table_mark) + self._table.bind("", self._download) + self._table.bind("", self._prev_tab) + self._table.bind("", self._next_tab) + self._table.bind("", self._mark_all) + + # Create entry boxes for URL & download_dir + infoframe.grid_columnconfigure(1, weight=1) + + info = [ + ("url", "Server Index:", self._set_url), + ("download_dir", "Download Directory:", self._set_download_dir), + ] + self._info = {} + for i, (key, label, callback) in enumerate(info): + tkinter.Label(infoframe, text=label).grid(column=0, row=i, sticky="e") + entry = tkinter.Entry( + infoframe, + font="courier", + relief="groove", + disabledforeground="#007aff", + foreground="#007aff", + ) + self._info[key] = (entry, callback) + entry.bind("", self._info_save) + entry.bind("", lambda e, key=key: self._info_edit(key)) + entry.grid(column=1, row=i, sticky="ew") + + # If the user edits url or download_dir, and then clicks outside + # the entry box, then save their results. + self.top.bind("", self._info_save) + + # Create Download & Refresh buttons. + self._download_button = tkinter.Button( + buttonframe, text="Download", command=self._download, width=8 + ) + self._download_button.pack(side="left") + self._refresh_button = tkinter.Button( + buttonframe, text="Refresh", command=self._refresh, width=8 + ) + self._refresh_button.pack(side="right") + + # Create Progress bar + self._progresslabel = tkinter.Label( + progressframe, + text="", + foreground=self._BACKDROP_COLOR[0], + background=self._BACKDROP_COLOR[1], + ) + self._progressbar = tkinter.Canvas( + progressframe, + width=200, + height=16, + background=self._PROGRESS_COLOR[1], + relief="sunken", + border=1, + ) + self._init_progressbar() + self._progressbar.pack(side="right") + self._progresslabel.pack(side="left") + + def _init_menu(self): + import tkinter + + menubar = tkinter.Menu(self.top) + + filemenu = tkinter.Menu(menubar, tearoff=0) + filemenu.add_command( + label="Download", underline=0, command=self._download, accelerator="Return" + ) + filemenu.add_separator() + filemenu.add_command( + label="Change Server Index", + underline=7, + command=lambda: self._info_edit("url"), + ) + filemenu.add_command( + label="Change Download Directory", + underline=0, + command=lambda: self._info_edit("download_dir"), + ) + filemenu.add_separator() + filemenu.add_command(label="Show Log", underline=5, command=self._show_log) + filemenu.add_separator() + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + # Create a menu to control which columns of the table are + # shown. n.b.: we never hide the first two columns (mark and + # identifier). + viewmenu = tkinter.Menu(menubar, tearoff=0) + for column in self._table.column_names[2:]: + var = tkinter.IntVar(self.top) + assert column not in self._column_vars + self._column_vars[column] = var + if column in self.INITIAL_COLUMNS: + var.set(1) + viewmenu.add_checkbutton( + label=column, underline=0, variable=var, command=self._select_columns + ) + menubar.add_cascade(label="View", underline=0, menu=viewmenu) + + # Create a sort menu + # [xx] this should be selectbuttons; and it should include + # reversed sorts as options. + sortmenu = tkinter.Menu(menubar, tearoff=0) + for column in self._table.column_names[1:]: + sortmenu.add_command( + label="Sort by %s" % column, + command=(lambda c=column: self._table.sort_by(c, "ascending")), + ) + sortmenu.add_separator() + # sortmenu.add_command(label='Descending Sort:') + for column in self._table.column_names[1:]: + sortmenu.add_command( + label="Reverse sort by %s" % column, + command=(lambda c=column: self._table.sort_by(c, "descending")), + ) + menubar.add_cascade(label="Sort", underline=0, menu=sortmenu) + + helpmenu = tkinter.Menu(menubar, tearoff=0) + helpmenu.add_command(label="About", underline=0, command=self.about) + helpmenu.add_command( + label="Instructions", underline=0, command=self.help, accelerator="F1" + ) + menubar.add_cascade(label="Help", underline=0, menu=helpmenu) + self.top.bind("", self.help) + + self.top.config(menu=menubar) + + def _select_columns(self): + for column, var in self._column_vars.items(): + if var.get(): + self._table.show_column(column) + else: + self._table.hide_column(column) + + def _refresh(self): + from tkinter.messagebox import showerror + + self._ds.clear_status_cache() + try: + self._fill_table() + except HTTPError as e: + showerror("Error reading from server", e) + except URLError as e: + showerror("Error connecting to server", e.reason) + self._table.select(0) + + def _info_edit(self, info_key): + self._info_save() # just in case. + entry, callback = self._info[info_key] + entry["state"] = "normal" + entry["relief"] = "sunken" + entry.focus() + + def _info_save(self, e=None): + focus = self._table + for entry, callback in self._info.values(): + if entry["state"] == "disabled": + continue + if e is not None and e.widget is entry and e.keysym != "Return": + focus = entry + else: + entry["state"] = "disabled" + entry["relief"] = "groove" + callback(entry.get()) + focus.focus() + + def _table_reprfunc(self, row, col, val): + if self._table.column_names[col].endswith("Size"): + if isinstance(val, str): + return " %s" % val + elif val < 1024**2: + return " %.1f KB" % (val / 1024.0**1) + elif val < 1024**3: + return " %.1f MB" % (val / 1024.0**2) + else: + return " %.1f GB" % (val / 1024.0**3) + + if col in (0, ""): + return str(val) + else: + return " %s" % val + + def _set_url(self, url): + from tkinter.messagebox import showerror + + if url == self._ds.url: + return + try: + self._ds.url = url + self._fill_table() + except OSError as e: + showerror("Error Setting Server Index", str(e)) + self._show_info() + + def _set_download_dir(self, download_dir): + from tkinter.messagebox import showerror + + if self._ds.download_dir == download_dir: + return + # check if the dir exists, and if not, ask if we should create it? + + # Clear our status cache, & re-check what's installed + self._ds.download_dir = download_dir + try: + self._fill_table() + except HTTPError as e: + showerror("Error reading from server", e) + except URLError as e: + showerror("Error connecting to server", e.reason) + self._show_info() + + def _show_info(self): + print("showing info", self._ds.url) + for entry, cb in self._info.values(): + entry["state"] = "normal" + entry.delete(0, "end") + self._info["url"][0].insert(0, self._ds.url) + self._info["download_dir"][0].insert(0, self._ds.download_dir) + for entry, cb in self._info.values(): + entry["state"] = "disabled" + + def _prev_tab(self, *e): + from tkinter.messagebox import showerror + + for i, tab in enumerate(self._tab_names): + if tab.lower() == self._tab and i > 0: + self._tab = self._tab_names[i - 1].lower() + try: + return self._fill_table() + except HTTPError as e: + showerror("Error reading from server", e) + except URLError as e: + showerror("Error connecting to server", e.reason) + + def _next_tab(self, *e): + from tkinter.messagebox import showerror + + for i, tab in enumerate(self._tab_names): + if tab.lower() == self._tab and i < (len(self._tabs) - 1): + self._tab = self._tab_names[i + 1].lower() + try: + return self._fill_table() + except HTTPError as e: + showerror("Error reading from server", e) + except URLError as e: + showerror("Error connecting to server", e.reason) + + def _select_tab(self, event): + from tkinter.messagebox import showerror + + self._tab = event.widget["text"].lower() + try: + self._fill_table() + except HTTPError as e: + showerror("Error reading from server", e) + except URLError as e: + showerror("Error connecting to server", e.reason) + + _tab = "collections" + # _tab = 'corpora' + _rows = None + + def _fill_table(self): + selected_row = self._table.selected_row() + self._table.clear() + if self._tab == "all packages": + items = self._ds.packages() + elif self._tab == "corpora": + items = self._ds.corpora() + elif self._tab == "models": + items = self._ds.models() + elif self._tab == "collections": + items = self._ds.collections() + else: + assert 0, "bad tab value %r" % self._tab + rows = [self._package_to_columns(item) for item in items] + self._table.extend(rows) + + # Highlight the active tab. + for tab, label in self._tabs.items(): + if tab == self._tab: + label.configure( + foreground=self._FRONT_TAB_COLOR[0], + background=self._FRONT_TAB_COLOR[1], + ) + else: + label.configure( + foreground=self._BACK_TAB_COLOR[0], + background=self._BACK_TAB_COLOR[1], + ) + + self._table.sort_by("Identifier", order="ascending") + self._color_table() + self._table.select(selected_row) + + # This is a hack, because the scrollbar isn't updating its + # position right -- I'm not sure what the underlying cause is + # though. (This is on OS X w/ python 2.5) The length of + # delay that's necessary seems to depend on how fast the + # comptuer is. :-/ + self.top.after(150, self._table._scrollbar.set, *self._table._mlb.yview()) + self.top.after(300, self._table._scrollbar.set, *self._table._mlb.yview()) + + def _update_table_status(self): + for row_num in range(len(self._table)): + status = self._ds.status(self._table[row_num, "Identifier"]) + self._table[row_num, "Status"] = status + self._color_table() + + def _download(self, *e): + # If we're using threads, then delegate to the threaded + # downloader instead. + if self._use_threads: + return self._download_threaded(*e) + + marked = [ + self._table[row, "Identifier"] + for row in range(len(self._table)) + if self._table[row, 0] != "" + ] + selection = self._table.selected_row() + if not marked and selection is not None: + marked = [self._table[selection, "Identifier"]] + + download_iter = self._ds.incr_download(marked, self._ds.download_dir) + self._log_indent = 0 + self._download_cb(download_iter, marked) + + _DL_DELAY = 10 + + def _download_cb(self, download_iter, ids): + try: + msg = next(download_iter) + except StopIteration: + # self._fill_table(sort=False) + self._update_table_status() + afterid = self.top.after(10, self._show_progress, 0) + self._afterid["_download_cb"] = afterid + return + + def show(s): + self._progresslabel["text"] = s + self._log(s) + + if isinstance(msg, ProgressMessage): + self._show_progress(msg.progress) + elif isinstance(msg, ErrorMessage): + show(msg.message) + if msg.package is not None: + self._select(msg.package.id) + self._show_progress(None) + return # halt progress. + elif isinstance(msg, StartCollectionMessage): + show("Downloading collection %s" % msg.collection.id) + self._log_indent += 1 + elif isinstance(msg, StartPackageMessage): + show("Downloading package %s" % msg.package.id) + elif isinstance(msg, UpToDateMessage): + show("Package %s is up-to-date!" % msg.package.id) + # elif isinstance(msg, StaleMessage): + # show('Package %s is out-of-date or corrupt' % msg.package.id) + elif isinstance(msg, FinishDownloadMessage): + show("Finished downloading %r." % msg.package.id) + elif isinstance(msg, StartUnzipMessage): + show("Unzipping %s" % msg.package.filename) + elif isinstance(msg, FinishCollectionMessage): + self._log_indent -= 1 + show("Finished downloading collection %r." % msg.collection.id) + self._clear_mark(msg.collection.id) + elif isinstance(msg, FinishPackageMessage): + self._clear_mark(msg.package.id) + afterid = self.top.after(self._DL_DELAY, self._download_cb, download_iter, ids) + self._afterid["_download_cb"] = afterid + + def _select(self, id): + for row in range(len(self._table)): + if self._table[row, "Identifier"] == id: + self._table.select(row) + return + + def _color_table(self): + # Color rows according to status. + for row in range(len(self._table)): + bg, sbg = self._ROW_COLOR[self._table[row, "Status"]] + fg, sfg = ("black", "white") + self._table.rowconfig( + row, + foreground=fg, + selectforeground=sfg, + background=bg, + selectbackground=sbg, + ) + # Color the marked column + self._table.itemconfigure( + row, 0, foreground=self._MARK_COLOR[0], background=self._MARK_COLOR[1] + ) + + def _clear_mark(self, id): + for row in range(len(self._table)): + if self._table[row, "Identifier"] == id: + self._table[row, 0] = "" + + def _mark_all(self, *e): + for row in range(len(self._table)): + self._table[row, 0] = "X" + + def _table_mark(self, *e): + selection = self._table.selected_row() + if selection >= 0: + if self._table[selection][0] != "": + self._table[selection, 0] = "" + else: + self._table[selection, 0] = "X" + self._table.select(delta=1) + + def _show_log(self): + from nltk.draw.util import ShowText + + text = "\n".join(self._log_messages) + ShowText(self.top, "NLTK Downloader Log", text) + + def _package_to_columns(self, pkg): + """ + Given a package, return a list of values describing that + package, one for each column in ``self.COLUMNS``. + """ + row = [] + for column_index, column_name in enumerate(self.COLUMNS): + if column_index == 0: # Mark: + row.append("") + elif column_name == "Identifier": + row.append(pkg.id) + elif column_name == "Status": + row.append(self._ds.status(pkg)) + else: + attr = column_name.lower().replace(" ", "_") + row.append(getattr(pkg, attr, "n/a")) + return row + + # ///////////////////////////////////////////////////////////////// + # External Interface + # ///////////////////////////////////////////////////////////////// + + def destroy(self, *e): + if self._destroyed: + return + self.top.destroy() + self._destroyed = True + + def _destroy(self, *e): + if self.top is not None: + for afterid in self._afterid.values(): + self.top.after_cancel(afterid) + + # Abort any download in progress. + if self._downloading and self._use_threads: + self._abort_download() + + # Make sure the garbage collector destroys these now; + # otherwise, they may get destroyed when we're not in the main + # thread, which would make Tkinter unhappy. + self._column_vars.clear() + + def mainloop(self, *args, **kwargs): + self.top.mainloop(*args, **kwargs) + + # ///////////////////////////////////////////////////////////////// + # HELP + # ///////////////////////////////////////////////////////////////// + + HELP = textwrap.dedent( + """\ + This tool can be used to download a variety of corpora and models + that can be used with NLTK. Each corpus or model is distributed + in a single zip file, known as a \"package file.\" You can + download packages individually, or you can download pre-defined + collections of packages. + + When you download a package, it will be saved to the \"download + directory.\" A default download directory is chosen when you run + + the downloader; but you may also select a different download + directory. On Windows, the default download directory is + + + \"package.\" + + The NLTK downloader can be used to download a variety of corpora, + models, and other data packages. + + Keyboard shortcuts:: + [return]\t Download + [up]\t Select previous package + [down]\t Select next package + [left]\t Select previous tab + [right]\t Select next tab + """ + ) + + def help(self, *e): + from nltk.draw.util import ShowText + + # The default font's not very legible; try using 'fixed' instead. + try: + ShowText( + self.top, + "Help: NLTK Downloader", + self.HELP.strip(), + width=75, + font="fixed", + ) + except Exception: + ShowText(self.top, "Help: NLTK Downloader", self.HELP.strip(), width=75) + + def about(self, *e): + from nltk.draw.util import ShowText + + ABOUT = "NLTK Downloader\n" + "Written by Edward Loper" + TITLE = "About: NLTK Downloader" + try: + from tkinter.messagebox import Message + + Message(message=ABOUT, title=TITLE).show() + except ImportError: + ShowText(self.top, TITLE, ABOUT) + + # ///////////////////////////////////////////////////////////////// + # Progress Bar + # ///////////////////////////////////////////////////////////////// + + _gradient_width = 5 + + def _init_progressbar(self): + c = self._progressbar + width, height = int(c["width"]), int(c["height"]) + for i in range(0, (int(c["width"]) * 2) // self._gradient_width): + c.create_line( + i * self._gradient_width + 20, + -20, + i * self._gradient_width - height - 20, + height + 20, + width=self._gradient_width, + fill="#%02x0000" % (80 + abs(i % 6 - 3) * 12), + ) + c.addtag_all("gradient") + c.itemconfig("gradient", state="hidden") + + # This is used to display progress + c.addtag_withtag( + "redbox", c.create_rectangle(0, 0, 0, 0, fill=self._PROGRESS_COLOR[0]) + ) + + def _show_progress(self, percent): + c = self._progressbar + if percent is None: + c.coords("redbox", 0, 0, 0, 0) + c.itemconfig("gradient", state="hidden") + else: + width, height = int(c["width"]), int(c["height"]) + x = percent * int(width) // 100 + 1 + c.coords("redbox", 0, 0, x, height + 1) + + def _progress_alive(self): + c = self._progressbar + if not self._downloading: + c.itemconfig("gradient", state="hidden") + else: + c.itemconfig("gradient", state="normal") + x1, y1, x2, y2 = c.bbox("gradient") + if x1 <= -100: + c.move("gradient", (self._gradient_width * 6) - 4, 0) + else: + c.move("gradient", -4, 0) + afterid = self.top.after(200, self._progress_alive) + self._afterid["_progress_alive"] = afterid + + # ///////////////////////////////////////////////////////////////// + # Threaded downloader + # ///////////////////////////////////////////////////////////////// + + def _download_threaded(self, *e): + # If the user tries to start a new download while we're already + # downloading something, then abort the current download instead. + if self._downloading: + self._abort_download() + return + + # Change the 'download' button to an 'abort' button. + self._download_button["text"] = "Cancel" + + marked = [ + self._table[row, "Identifier"] + for row in range(len(self._table)) + if self._table[row, 0] != "" + ] + selection = self._table.selected_row() + if not marked and selection is not None: + marked = [self._table[selection, "Identifier"]] + + # Create a new data server object for the download operation, + # just in case the user modifies our data server during the + # download (e.g., clicking 'refresh' or editing the index url). + ds = Downloader(self._ds.url, self._ds.download_dir) + + # Start downloading in a separate thread. + assert self._download_msg_queue == [] + assert self._download_abort_queue == [] + self._DownloadThread( + ds, + marked, + self._download_lock, + self._download_msg_queue, + self._download_abort_queue, + ).start() + + # Monitor the download message queue & display its progress. + self._log_indent = 0 + self._downloading = True + self._monitor_message_queue() + + # Display an indication that we're still alive and well by + # cycling the progress bar. + self._progress_alive() + + def _abort_download(self): + if self._downloading: + self._download_lock.acquire() + self._download_abort_queue.append("abort") + self._download_lock.release() + + class _DownloadThread(threading.Thread): + def __init__(self, data_server, items, lock, message_queue, abort): + self.data_server = data_server + self.items = items + self.lock = lock + self.message_queue = message_queue + self.abort = abort + threading.Thread.__init__(self) + + def run(self): + for msg in self.data_server.incr_download(self.items): + self.lock.acquire() + self.message_queue.append(msg) + # Check if we've been told to kill ourselves: + if self.abort: + self.message_queue.append("aborted") + self.lock.release() + return + self.lock.release() + self.lock.acquire() + self.message_queue.append("finished") + self.lock.release() + + _MONITOR_QUEUE_DELAY = 100 + + def _monitor_message_queue(self): + def show(s): + self._progresslabel["text"] = s + self._log(s) + + # Try to acquire the lock; if it's busy, then just try again later. + if not self._download_lock.acquire(): + return + for msg in self._download_msg_queue: + # Done downloading? + if msg == "finished" or msg == "aborted": + # self._fill_table(sort=False) + self._update_table_status() + self._downloading = False + self._download_button["text"] = "Download" + del self._download_msg_queue[:] + del self._download_abort_queue[:] + self._download_lock.release() + if msg == "aborted": + show("Download aborted!") + self._show_progress(None) + else: + afterid = self.top.after(100, self._show_progress, None) + self._afterid["_monitor_message_queue"] = afterid + return + + # All other messages + elif isinstance(msg, ProgressMessage): + self._show_progress(msg.progress) + elif isinstance(msg, ErrorMessage): + show(msg.message) + if msg.package is not None: + self._select(msg.package.id) + self._show_progress(None) + self._downloading = False + return # halt progress. + elif isinstance(msg, StartCollectionMessage): + show("Downloading collection %r" % msg.collection.id) + self._log_indent += 1 + elif isinstance(msg, StartPackageMessage): + self._ds.clear_status_cache(msg.package.id) + show("Downloading package %r" % msg.package.id) + elif isinstance(msg, UpToDateMessage): + show("Package %s is up-to-date!" % msg.package.id) + # elif isinstance(msg, StaleMessage): + # show('Package %s is out-of-date or corrupt; updating it' % + # msg.package.id) + elif isinstance(msg, FinishDownloadMessage): + show("Finished downloading %r." % msg.package.id) + elif isinstance(msg, StartUnzipMessage): + show("Unzipping %s" % msg.package.filename) + elif isinstance(msg, FinishUnzipMessage): + show("Finished installing %s" % msg.package.id) + elif isinstance(msg, FinishCollectionMessage): + self._log_indent -= 1 + show("Finished downloading collection %r." % msg.collection.id) + self._clear_mark(msg.collection.id) + elif isinstance(msg, FinishPackageMessage): + self._update_table_status() + self._clear_mark(msg.package.id) + + # Let the user know when we're aborting a download (but + # waiting for a good point to abort it, so we don't end up + # with a partially unzipped package or anything like that). + if self._download_abort_queue: + self._progresslabel["text"] = "Aborting download..." + + # Clear the message queue and then release the lock + del self._download_msg_queue[:] + self._download_lock.release() + + # Check the queue again after MONITOR_QUEUE_DELAY msec. + afterid = self.top.after(self._MONITOR_QUEUE_DELAY, self._monitor_message_queue) + self._afterid["_monitor_message_queue"] = afterid + + +###################################################################### +# Helper Functions +###################################################################### +# [xx] It may make sense to move these to nltk.internals. + + +def md5_hexdigest(file): + """ + Calculate and return the MD5 checksum for a given file. + ``file`` may either be a filename or an open stream. + """ + if isinstance(file, str): + with open(file, "rb") as infile: + return _md5_hexdigest(infile) + return _md5_hexdigest(file) + + +def _md5_hexdigest(fp): + md5_digest = md5() + while True: + block = fp.read(1024 * 16) # 16k blocks + if not block: + break + md5_digest.update(block) + return md5_digest.hexdigest() + + +def sha256_hexdigest(file): + """ + Calculate and return the SHA-256 checksum for a given file. + ``file`` may either be a filename or an open stream. + """ + if isinstance(file, str): + with open(file, "rb") as infile: + return _sha256_hexdigest(infile) + return _sha256_hexdigest(file) + + +def _sha256_hexdigest(fp): + sha256_digest = sha256() + while True: + block = fp.read(1024 * 16) # 16k blocks + if not block: + break + sha256_digest.update(block) + return sha256_digest.hexdigest() + + +# change this to periodically yield progress messages? +# [xx] get rid of topdir parameter -- we should be checking +# this when we build the index, anyway. +def unzip(filename, root, verbose=True): + """ + Extract the contents of the zip file ``filename`` into the + directory ``root``. + """ + for message in _unzip_iter(filename, root, verbose): + if isinstance(message, ErrorMessage): + raise Exception(message) + + +def _validate_member(member, root_abs): + """ + Check a single ZIP member name for path-traversal and escape vectors. + + Returns ``None`` if the member is safe, or a human-readable error + string if the member must be rejected. Comparison prefixes are + derived from *root_abs* internally via ``os.path.normcase`` so that + callers cannot supply inconsistent values. + + Parameters + ---------- + member : str + The archive entry name (forward-slash separated, as stored in the ZIP). + .. note:: + This function enforces Zip-Slip and symlink-escape containment only. + Cross-package ownership (CVE-2026-12261) is enforced separately by + the ``expected_root`` parameter of ``_unzip_iter``. + + root_abs : str + Absolute path of the extraction root (used to build candidate paths + and derive comparison prefixes via ``os.path.normcase``). + """ + if "\x00" in member: + return f"Null byte in entry name blocked: {member!r}" + + abs_prefix = os.path.normcase(root_abs).rstrip(os.sep) + os.sep + target_abs = os.path.normcase(os.path.abspath(os.path.join(root_abs, member))) + if not target_abs.startswith(abs_prefix): + return f"Zip Slip blocked: {member}" + + real_prefix = os.path.normcase(os.path.realpath(root_abs)).rstrip(os.sep) + os.sep + target_real = os.path.normcase(os.path.realpath(os.path.join(root_abs, member))) + if not target_real.startswith(real_prefix): + return f"Symlink escape blocked: {member}" + + return None + + +def _unzip_iter(filename, root, verbose=True, expected_root=None): + """ + Secure ZIP extraction using validate-then-extract. + + All members are validated before any extraction occurs. If any member + fails validation the entire archive is rejected and nothing is written + to disk. + + Checks performed on every member name: + - Null-byte rejection (platform path-truncation vector) + - Zip-Slip (.., absolute paths, drive letters) + - Symlink-escape (writes through pre-existing symlinks) + + All path comparisons use ``os.path.normcase`` so that the checks + are case-insensitive on Windows (no-op on POSIX). + + Members are validated again immediately before each extraction as a + best-effort check against filesystem changes between Phase 1 and the + actual write, but this cannot eliminate TOCTOU races inherent in + non-atomic extraction. + + Extraction aborts on the first error (including I/O errors) rather + than skipping individual members. Partial extraction of a potentially + malicious archive is worse than no extraction. + """ + + if verbose: + sys.stdout.write("Unzipping %s" % os.path.split(filename)[1]) + sys.stdout.flush() + + try: + zf = ZipFile(filename) + except Exception as e: + yield ErrorMessage(filename, e) + # Flush the "Unzipping ..." line here because the try/finally that + # normally handles this is never entered (zf was never assigned). + if verbose: + print() + return + + try: + root_abs = os.path.abspath(root) + members = zf.namelist() + + # Phase 1 -- validate every member before touching the filesystem. + has_violations = False + for member in members: + # --- CVE-2026-12261 fix (cross-package ownership check) --- + # Reject any member whose top-level path component differs from + # the owning package id. This blocks cross-package overwrite + # without path traversal: a member like + # averaged_perceptron_tagger_eng/file.json + # inside a package with id='evil-corpus' is rejected because + # 'averaged_perceptron_tagger_eng' != 'evil-corpus'. + if expected_root is not None: + top = member.replace("\\", "/").lstrip("/").split("/")[0] + if top != expected_root: + yield ErrorMessage( + filename, + f"Cross-package overwrite blocked: member {member!r} " + f"is outside the owning package directory {expected_root!r}", + ) + has_violations = True + continue + # ----------------------------------------------------------- + error = _validate_member(member, root_abs) + if error is not None: + yield ErrorMessage(filename, error) + has_violations = True + continue + # Decompression-bomb check belongs in Phase 1: a bomb member must be + # rejected before any (earlier, benign) member is written to disk, so + # the validate-then-extract / nothing-is-written contract holds. + try: + _check_decompression_bomb(zf.getinfo(member)) + except ValueError as e: + yield ErrorMessage(filename, str(e)) + has_violations = True + + if has_violations: + return + + # Phase 2 -- all members passed; extract. + try: + os.makedirs(root_abs, exist_ok=True) + except Exception as e: + yield ErrorMessage(filename, f"Extraction error: {e}") + return + + for member in members: + if expected_root is not None: + top = member.replace("\\", "/").lstrip("/").split("/")[0] + if top != expected_root: + yield ErrorMessage( + filename, + f"Cross-package overwrite blocked: member {member!r} " + f"is outside the owning package directory {expected_root!r}", + ) + return + error = _validate_member(member, root_abs) + if error is not None: + yield ErrorMessage(filename, f"{error} (during extraction)") + return + try: + zf.extract(member, root_abs) + except Exception as e: + yield ErrorMessage(filename, f"Extraction error for {member}: {e}") + return + except Exception as e: + yield ErrorMessage(filename, f"Validation error: {e}") + finally: + zf.close() + if verbose: + print() + + +###################################################################### +# Index Builder +###################################################################### +# This may move to a different file sometime. + + +def build_index(root, base_url): + """ + Create a new data.xml index file, by combining the xml description + files for various packages and collections. ``root`` should be the + path to a directory containing the package xml and zip files; and + the collection xml files. The ``root`` directory is expected to + have the following subdirectories:: + + root/ + packages/ .................. subdirectory for packages + corpora/ ................. zip & xml files for corpora + grammars/ ................ zip & xml files for grammars + taggers/ ................. zip & xml files for taggers + tokenizers/ .............. zip & xml files for tokenizers + etc. + collections/ ............... xml files for collections + + For each package, there should be two files: ``package.zip`` + (where *package* is the package name) + which contains the package itself as a compressed zip file; and + ``package.xml``, which is an xml description of the package. The + zipfile ``package.zip`` should expand to a single subdirectory + named ``package/``. The base filename ``package`` must match + the identifier given in the package's xml file. + + For each collection, there should be a single file ``collection.zip`` + describing the collection, where *collection* is the name of the collection. + + All identifiers (for both packages and collections) must be unique. + """ + # Find all packages. + packages = [] + for pkg_xml, zf, subdir in _find_packages(os.path.join(root, "packages")): + zipstat = os.stat(zf.filename) + url = f"{base_url}/{subdir}/{os.path.split(zf.filename)[1]}" + unzipped_size = sum(zf_info.file_size for zf_info in zf.infolist()) + + # Fill in several fields of the package xml with calculated values. + pkg_xml.set("unzipped_size", "%s" % unzipped_size) + pkg_xml.set("size", "%s" % zipstat.st_size) + pkg_xml.set("checksum", "%s" % md5_hexdigest(zf.filename)) + pkg_xml.set("sha256_checksum", "%s" % sha256_hexdigest(zf.filename)) + pkg_xml.set("subdir", subdir) + # pkg_xml.set('svn_revision', _svn_revision(zf.filename)) + if not pkg_xml.get("url"): + pkg_xml.set("url", url) + + # Record the package. + packages.append(pkg_xml) + + # Find all collections + collections = list(_find_collections(os.path.join(root, "collections"))) + + # Check that all UIDs are unique + uids = set() + for item in packages + collections: + if item.get("id") in uids: + raise ValueError("Duplicate UID: %s" % item.get("id")) + uids.add(item.get("id")) + + # Put it all together + top_elt = ElementTree.Element("nltk_data") + top_elt.append(ElementTree.Element("packages")) + top_elt[0].extend(sorted(packages, key=lambda package: package.get("id"))) + top_elt.append(ElementTree.Element("collections")) + top_elt[1].extend(sorted(collections, key=lambda collection: collection.get("id"))) + + _indent_xml(top_elt) + return top_elt + + +def _indent_xml(xml, prefix=""): + """ + Helper for ``build_index()``: Given an XML ``ElementTree``, modify it + (and its descendents) ``text`` and ``tail`` attributes to generate + an indented tree, where each nested element is indented by 2 + spaces with respect to its parent. + """ + if len(xml) > 0: + xml.text = (xml.text or "").strip() + "\n" + prefix + " " + for child in xml: + _indent_xml(child, prefix + " ") + for child in xml[:-1]: + child.tail = (child.tail or "").strip() + "\n" + prefix + " " + xml[-1].tail = (xml[-1].tail or "").strip() + "\n" + prefix + + +def _check_package(pkg_xml, zipfilename, zf): + """ + Helper for ``build_index()``: Perform some checks to make sure that + the given package is consistent. + """ + # The filename must patch the id given in the XML file. + uid = os.path.splitext(os.path.split(zipfilename)[1])[0] + if pkg_xml.get("id") != uid: + raise ValueError( + "package identifier mismatch ({} vs {})".format(pkg_xml.get("id"), uid) + ) + + # Zip file must expand to a subdir whose name matches uid. + if sum((name != uid and not name.startswith(uid + "/")) for name in zf.namelist()): + raise ValueError( + "Zipfile {}.zip does not expand to a single subdirectory {}/".format( + uid, uid + ) + ) + + +# update for git? +def _svn_revision(filename): + """ + Helper for ``build_index()``: Calculate the subversion revision + number for a given file (by using ``subprocess`` to run ``svn``). + """ + p = subprocess.Popen( + ["svn", "status", "-v", filename], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + if p.returncode != 0 or stderr or not stdout: + raise ValueError( + "Error determining svn_revision for %s: %s" + % (os.path.split(filename)[1], textwrap.fill(stderr)) + ) + return stdout.split()[2] + + +def _find_collections(root): + """ + Helper for ``build_index()``: Yield a list of ElementTree.Element + objects, each holding the xml for a single package collection. + """ + for dirname, _subdirs, files in os.walk(root): + for filename in files: + if filename.endswith(".xml"): + xmlfile = os.path.join(dirname, filename) + yield ElementTree.parse(xmlfile).getroot() + + +def _find_packages(root): + """ + Helper for ``build_index()``: Yield a list of tuples + ``(pkg_xml, zf, subdir)``, where: + - ``pkg_xml`` is an ``ElementTree.Element`` holding the xml for a + package + - ``zf`` is a ``ZipFile`` for the package's contents. + - ``subdir`` is the subdirectory (relative to ``root``) where + the package was found (e.g. 'corpora' or 'grammars'). + """ + from nltk.corpus.reader.util import _path_from + + # Find all packages. + packages = [] + for dirname, subdirs, files in os.walk(root): + relpath = "/".join(_path_from(root, dirname)) + for filename in files: + if filename.endswith(".xml"): + xmlfilename = os.path.join(dirname, filename) + zipfilename = xmlfilename[:-4] + ".zip" + try: + zf = ZipFile(zipfilename) + except Exception as e: + raise ValueError(f"Error reading file {zipfilename!r}!\n{e}") from e + try: + pkg_xml = ElementTree.parse(xmlfilename).getroot() + except Exception as e: + raise ValueError(f"Error reading file {xmlfilename!r}!\n{e}") from e + + # Check that the UID matches the filename + uid = os.path.split(xmlfilename[:-4])[1] + if pkg_xml.get("id") != uid: + raise ValueError( + "package identifier mismatch (%s " + "vs %s)" % (pkg_xml.get("id"), uid) + ) + + # Check that the zipfile expands to a subdir whose + # name matches the uid. + if sum( + (name != uid and not name.startswith(uid + "/")) + for name in zf.namelist() + ): + raise ValueError( + "Zipfile %s.zip does not expand to a " + "single subdirectory %s/" % (uid, uid) + ) + + yield pkg_xml, zf, relpath + + elif filename.endswith(".zip"): + # Warn user in case a .xml does not exist for a .zip + resourcename = os.path.splitext(filename)[0] + xmlfilename = os.path.join(dirname, resourcename + ".xml") + if not os.path.exists(xmlfilename): + warnings.warn( + f"{filename} exists, but {resourcename + '.xml'} cannot be found! " + f"This could mean that {resourcename} can not be downloaded.", + stacklevel=2, + ) + + # Don't recurse into svn subdirectories: + try: + subdirs.remove(".svn") + except ValueError: + pass + + +###################################################################### +# Main: +###################################################################### + +# There should be a command-line interface + +# Aliases +_downloader = Downloader() +download = _downloader.download + + +def download_shell(): + DownloaderShell(_downloader).run() + + +def download_gui(): + DownloaderGUI(_downloader).mainloop() + + +def update(): + _downloader.update() + + +if __name__ == "__main__": + from optparse import OptionParser + + parser = OptionParser() + parser.add_option( + "-d", + "--dir", + dest="dir", + help="download package to directory DIR", + metavar="DIR", + ) + parser.add_option( + "-q", + "--quiet", + dest="quiet", + action="store_true", + default=False, + help="work quietly", + ) + parser.add_option( + "-f", + "--force", + dest="force", + action="store_true", + default=False, + help="download even if already installed", + ) + parser.add_option( + "-e", + "--exit-on-error", + dest="halt_on_error", + action="store_true", + default=False, + help="exit if an error occurs", + ) + parser.add_option( + "-u", + "--url", + dest="server_index_url", + default=os.environ.get("NLTK_DOWNLOAD_URL"), + help="download server index url", + ) + + options, args = parser.parse_args() + + downloader = Downloader(server_index_url=options.server_index_url) + + if args: + for pkg_id in args: + rv = downloader.download( + info_or_id=pkg_id, + download_dir=options.dir, + quiet=options.quiet, + force=options.force, + halt_on_error=options.halt_on_error, + ) + if not rv and options.halt_on_error: + break + else: + downloader.download( + download_dir=options.dir, + quiet=options.quiet, + force=options.force, + halt_on_error=options.halt_on_error, + ) diff --git a/nltk/draw/__init__.py b/nltk/draw/__init__.py new file mode 100644 index 0000000..4bedac6 --- /dev/null +++ b/nltk/draw/__init__.py @@ -0,0 +1,27 @@ +# Natural Language Toolkit: graphical representations package +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +# Import Tkinter-based modules if Tkinter is installed +try: + import tkinter +except ImportError: + import warnings + + warnings.warn("nltk.draw package not loaded (please install Tkinter library).") +else: + from nltk.draw.cfg import ProductionList, CFGEditor, CFGDemo + from nltk.draw.tree import ( + TreeSegmentWidget, + tree_to_treesegment, + TreeWidget, + TreeView, + draw_trees, + ) + from nltk.draw.table import Table + +from nltk.draw.dispersion import dispersion_plot diff --git a/nltk/draw/cfg.py b/nltk/draw/cfg.py new file mode 100644 index 0000000..a897130 --- /dev/null +++ b/nltk/draw/cfg.py @@ -0,0 +1,859 @@ +# Natural Language Toolkit: CFG visualization +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Visualization tools for CFGs. +""" + +# Idea for a nice demo: +# - 3 panes: grammar, treelet, working area +# - grammar is a list of productions +# - when you select a production, the treelet that it licenses appears +# in the treelet area +# - the working area has the text on the bottom, and S at top. When +# you select a production, it shows (ghosted) the locations where +# that production's treelet could be attached to either the text +# or the tree rooted at S. +# - the user can drag the treelet onto one of those (or click on them?) +# - the user can delete pieces of the tree from the working area +# (right click?) +# - connecting top to bottom? drag one NP onto another? +# +# +-------------------------------------------------------------+ +# | S -> NP VP | S | +# |[NP -> Det N ]| / \ | +# | ... | NP VP | +# | N -> 'dog' | | +# | N -> 'cat' | | +# | ... | | +# +--------------+ | +# | NP | Det N | +# | / \ | | | | +# | Det N | the cat saw the dog | +# | | | +# +--------------+----------------------------------------------+ +# +# Operations: +# - connect a new treelet -- drag or click shadow +# - delete a treelet -- right click +# - if only connected to top, delete everything below +# - if only connected to bottom, delete everything above +# - connect top & bottom -- drag a leaf to a root or a root to a leaf +# - disconnect top & bottom -- right click +# - if connected to top & bottom, then disconnect + +import re +from tkinter import ( + Button, + Canvas, + Entry, + Frame, + IntVar, + Label, + Scrollbar, + Text, + Tk, + Toplevel, +) + +from nltk.draw.tree import TreeSegmentWidget, tree_to_treesegment +from nltk.draw.util import ( + CanvasFrame, + ColorizedList, + ShowText, + SymbolWidget, + TextWidget, +) +from nltk.grammar import CFG, Nonterminal, _read_cfg_production, nonterminals +from nltk.tree import Tree + +###################################################################### +# Production List +###################################################################### + + +class ProductionList(ColorizedList): + ARROW = SymbolWidget.SYMBOLS["rightarrow"] + + def _init_colortags(self, textwidget, options): + textwidget.tag_config("terminal", foreground="#006000") + textwidget.tag_config("arrow", font="symbol", underline="0") + textwidget.tag_config( + "nonterminal", foreground="blue", font=("helvetica", -12, "bold") + ) + + def _item_repr(self, item): + contents = [] + contents.append(("%s\t" % item.lhs(), "nonterminal")) + contents.append((self.ARROW, "arrow")) + for elt in item.rhs(): + if isinstance(elt, Nonterminal): + contents.append((" %s" % elt.symbol(), "nonterminal")) + else: + contents.append((" %r" % elt, "terminal")) + return contents + + +###################################################################### +# CFG Editor +###################################################################### + +_CFGEditor_HELP = """ + +The CFG Editor can be used to create or modify context free grammars. +A context free grammar consists of a start symbol and a list of +productions. The start symbol is specified by the text entry field in +the upper right hand corner of the editor; and the list of productions +are specified in the main text editing box. + +Every non-blank line specifies a single production. Each production +has the form "LHS -> RHS," where LHS is a single nonterminal, and RHS +is a list of nonterminals and terminals. + +Nonterminals must be a single word, such as S or NP or NP_subj. +Currently, nonterminals must consists of alphanumeric characters and +underscores (_). Nonterminals are colored blue. If you place the +mouse over any nonterminal, then all occurrences of that nonterminal +will be highlighted. + +Terminals must be surrounded by single quotes (') or double +quotes(\"). For example, "dog" and "New York" are terminals. +Currently, the string within the quotes must consist of alphanumeric +characters, underscores, and spaces. + +To enter a new production, go to a blank line, and type a nonterminal, +followed by an arrow (->), followed by a sequence of terminals and +nonterminals. Note that "->" (dash + greater-than) is automatically +converted to an arrow symbol. When you move your cursor to a +different line, your production will automatically be colorized. If +there are any errors, they will be highlighted in red. + +Note that the order of the productions is significant for some +algorithms. To re-order the productions, use cut and paste to move +them. + +Use the buttons at the bottom of the window when you are done editing +the CFG: + - Ok: apply the new CFG, and exit the editor. + - Apply: apply the new CFG, and do not exit the editor. + - Reset: revert to the original CFG, and do not exit the editor. + - Cancel: revert to the original CFG, and exit the editor. + +""" + + +class CFGEditor: + """ + A dialog window for creating and editing context free grammars. + ``CFGEditor`` imposes the following restrictions: + + - All nonterminals must be strings consisting of word + characters. + - All terminals must be strings consisting of word characters + and space characters. + """ + + # Regular expressions used by _analyze_line. Precompile them, so + # we can process the text faster. + ARROW = SymbolWidget.SYMBOLS["rightarrow"] + _LHS_RE = re.compile(r"(^\s*\w+\s*)(->|(" + ARROW + "))") + _ARROW_RE = re.compile(r"\s*(->|(" + ARROW + r"))\s*") + _PRODUCTION_RE = re.compile( + r"(^\s*\w+\s*)" + + "(->|(" # LHS + + ARROW + + r"))\s*" + + r"((\w+|'[\w ]*'|\"[\w ]*\"|\|)\s*)*$" # arrow + ) # RHS + _TOKEN_RE = re.compile("\\w+|->|'[\\w ]+'|\"[\\w ]+\"|(" + ARROW + ")") + _BOLD = ("helvetica", -12, "bold") + + def __init__(self, parent, cfg=None, set_cfg_callback=None): + self._parent = parent + if cfg is not None: + self._cfg = cfg + else: + self._cfg = CFG(Nonterminal("S"), []) + self._set_cfg_callback = set_cfg_callback + + self._highlight_matching_nonterminals = 1 + + # Create the top-level window. + self._top = Toplevel(parent) + self._init_bindings() + + self._init_startframe() + self._startframe.pack(side="top", fill="x", expand=0) + self._init_prodframe() + self._prodframe.pack(side="top", fill="both", expand=1) + self._init_buttons() + self._buttonframe.pack(side="bottom", fill="x", expand=0) + + self._textwidget.focus() + + def _init_startframe(self): + frame = self._startframe = Frame(self._top) + self._start = Entry(frame) + self._start.pack(side="right") + Label(frame, text="Start Symbol:").pack(side="right") + Label(frame, text="Productions:").pack(side="left") + self._start.insert(0, self._cfg.start().symbol()) + + def _init_buttons(self): + frame = self._buttonframe = Frame(self._top) + Button(frame, text="Ok", command=self._ok, underline=0, takefocus=0).pack( + side="left" + ) + Button(frame, text="Apply", command=self._apply, underline=0, takefocus=0).pack( + side="left" + ) + Button(frame, text="Reset", command=self._reset, underline=0, takefocus=0).pack( + side="left" + ) + Button( + frame, text="Cancel", command=self._cancel, underline=0, takefocus=0 + ).pack(side="left") + Button(frame, text="Help", command=self._help, underline=0, takefocus=0).pack( + side="right" + ) + + def _init_bindings(self): + self._top.title("CFG Editor") + self._top.bind("", self._cancel) + self._top.bind("", self._cancel) + self._top.bind("", self._cancel) + # self._top.bind('', self._cancel) + self._top.bind("", self._cancel) + self._top.bind("", self._cancel) + # self._top.bind('', self._cancel) + self._top.bind("", self._cancel) + + self._top.bind("", self._ok) + self._top.bind("", self._ok) + self._top.bind("", self._apply) + self._top.bind("", self._apply) + self._top.bind("", self._reset) + self._top.bind("", self._reset) + self._top.bind("", self._help) + self._top.bind("", self._help) + self._top.bind("", self._help) + + def _init_prodframe(self): + self._prodframe = Frame(self._top) + + # Create the basic Text widget & scrollbar. + self._textwidget = Text( + self._prodframe, background="#e0e0e0", exportselection=1 + ) + self._textscroll = Scrollbar(self._prodframe, takefocus=0, orient="vertical") + self._textwidget.config(yscrollcommand=self._textscroll.set) + self._textscroll.config(command=self._textwidget.yview) + self._textscroll.pack(side="right", fill="y") + self._textwidget.pack(expand=1, fill="both", side="left") + + # Initialize the colorization tags. Each nonterminal gets its + # own tag, so they aren't listed here. + self._textwidget.tag_config("terminal", foreground="#006000") + self._textwidget.tag_config("arrow", font="symbol") + self._textwidget.tag_config("error", background="red") + + # Keep track of what line they're on. We use that to remember + # to re-analyze a line whenever they leave it. + self._linenum = 0 + + # Expand "->" to an arrow. + self._top.bind(">", self._replace_arrows) + + # Re-colorize lines when appropriate. + self._top.bind("<>", self._analyze) + self._top.bind("", self._check_analyze) + self._top.bind("", self._check_analyze) + + # Tab cycles focus. (why doesn't this work??) + def cycle(e, textwidget=self._textwidget): + textwidget.tk_focusNext().focus() + + self._textwidget.bind("", cycle) + + prod_tuples = [(p.lhs(), [p.rhs()]) for p in self._cfg.productions()] + for i in range(len(prod_tuples) - 1, 0, -1): + if prod_tuples[i][0] == prod_tuples[i - 1][0]: + if () in prod_tuples[i][1]: + continue + if () in prod_tuples[i - 1][1]: + continue + print(prod_tuples[i - 1][1]) + print(prod_tuples[i][1]) + prod_tuples[i - 1][1].extend(prod_tuples[i][1]) + del prod_tuples[i] + + for lhs, rhss in prod_tuples: + print(lhs, rhss) + s = "%s ->" % lhs + for rhs in rhss: + for elt in rhs: + if isinstance(elt, Nonterminal): + s += " %s" % elt + else: + s += " %r" % elt + s += " |" + s = s[:-2] + "\n" + self._textwidget.insert("end", s) + + self._analyze() + + # # Add the productions to the text widget, and colorize them. + # prod_by_lhs = {} + # for prod in self._cfg.productions(): + # if len(prod.rhs()) > 0: + # prod_by_lhs.setdefault(prod.lhs(),[]).append(prod) + # for (lhs, prods) in prod_by_lhs.items(): + # self._textwidget.insert('end', '%s ->' % lhs) + # self._textwidget.insert('end', self._rhs(prods[0])) + # for prod in prods[1:]: + # print '\t|'+self._rhs(prod), + # self._textwidget.insert('end', '\t|'+self._rhs(prod)) + # print + # self._textwidget.insert('end', '\n') + # for prod in self._cfg.productions(): + # if len(prod.rhs()) == 0: + # self._textwidget.insert('end', '%s' % prod) + # self._analyze() + + # def _rhs(self, prod): + # s = '' + # for elt in prod.rhs(): + # if isinstance(elt, Nonterminal): s += ' %s' % elt.symbol() + # else: s += ' %r' % elt + # return s + + def _clear_tags(self, linenum): + """ + Remove all tags (except ``arrow`` and ``sel``) from the given + line of the text widget used for editing the productions. + """ + start = "%d.0" % linenum + end = "%d.end" % linenum + for tag in self._textwidget.tag_names(): + if tag not in ("arrow", "sel"): + self._textwidget.tag_remove(tag, start, end) + + def _check_analyze(self, *e): + """ + Check if we've moved to a new line. If we have, then remove + all colorization from the line we moved to, and re-colorize + the line that we moved from. + """ + linenum = int(self._textwidget.index("insert").split(".")[0]) + if linenum != self._linenum: + self._clear_tags(linenum) + self._analyze_line(self._linenum) + self._linenum = linenum + + def _replace_arrows(self, *e): + """ + Replace any ``'->'`` text strings with arrows (char \\256, in + symbol font). This searches the whole buffer, but is fast + enough to be done anytime they press '>'. + """ + arrow = "1.0" + while True: + arrow = self._textwidget.search("->", arrow, "end+1char") + if arrow == "": + break + self._textwidget.delete(arrow, arrow + "+2char") + self._textwidget.insert(arrow, self.ARROW, "arrow") + self._textwidget.insert(arrow, "\t") + + arrow = "1.0" + while True: + arrow = self._textwidget.search(self.ARROW, arrow + "+1char", "end+1char") + if arrow == "": + break + self._textwidget.tag_add("arrow", arrow, arrow + "+1char") + + def _analyze_token(self, match, linenum): + """ + Given a line number and a regexp match for a token on that + line, colorize the token. Note that the regexp match gives us + the token's text, start index (on the line), and end index (on + the line). + """ + # What type of token is it? + if match.group()[0] in "'\"": + tag = "terminal" + elif match.group() in ("->", self.ARROW): + tag = "arrow" + else: + # If it's a nonterminal, then set up new bindings, so we + # can highlight all instances of that nonterminal when we + # put the mouse over it. + tag = "nonterminal_" + match.group() + if tag not in self._textwidget.tag_names(): + self._init_nonterminal_tag(tag) + + start = "%d.%d" % (linenum, match.start()) + end = "%d.%d" % (linenum, match.end()) + self._textwidget.tag_add(tag, start, end) + + def _init_nonterminal_tag(self, tag, foreground="blue"): + self._textwidget.tag_config(tag, foreground=foreground, font=CFGEditor._BOLD) + if not self._highlight_matching_nonterminals: + return + + def enter(e, textwidget=self._textwidget, tag=tag): + textwidget.tag_config(tag, background="#80ff80") + + def leave(e, textwidget=self._textwidget, tag=tag): + textwidget.tag_config(tag, background="") + + self._textwidget.tag_bind(tag, "", enter) + self._textwidget.tag_bind(tag, "", leave) + + def _analyze_line(self, linenum): + """ + Colorize a given line. + """ + # Get rid of any tags that were previously on the line. + self._clear_tags(linenum) + + # Get the line line's text string. + line = self._textwidget.get(repr(linenum) + ".0", repr(linenum) + ".end") + + # If it's a valid production, then colorize each token. + if CFGEditor._PRODUCTION_RE.match(line): + # It's valid; Use _TOKEN_RE to tokenize the production, + # and call analyze_token on each token. + def analyze_token(match, self=self, linenum=linenum): + self._analyze_token(match, linenum) + return "" + + CFGEditor._TOKEN_RE.sub(analyze_token, line) + elif line.strip() != "": + # It's invalid; show the user where the error is. + self._mark_error(linenum, line) + + def _mark_error(self, linenum, line): + """ + Mark the location of an error in a line. + """ + arrowmatch = CFGEditor._ARROW_RE.search(line) + if not arrowmatch: + # If there's no arrow at all, highlight the whole line. + start = "%d.0" % linenum + end = "%d.end" % linenum + elif not CFGEditor._LHS_RE.match(line): + # Otherwise, if the LHS is bad, highlight it. + start = "%d.0" % linenum + end = "%d.%d" % (linenum, arrowmatch.start()) + else: + # Otherwise, highlight the RHS. + start = "%d.%d" % (linenum, arrowmatch.end()) + end = "%d.end" % linenum + + # If we're highlighting 0 chars, highlight the whole line. + if self._textwidget.compare(start, "==", end): + start = "%d.0" % linenum + end = "%d.end" % linenum + self._textwidget.tag_add("error", start, end) + + def _analyze(self, *e): + """ + Replace ``->`` with arrows, and colorize the entire buffer. + """ + self._replace_arrows() + numlines = int(self._textwidget.index("end").split(".")[0]) + for linenum in range(1, numlines + 1): # line numbers start at 1. + self._analyze_line(linenum) + + def _parse_productions(self): + """ + Parse the current contents of the textwidget buffer, to create + a list of productions. + """ + productions = [] + + # Get the text, normalize it, and split it into lines. + text = self._textwidget.get("1.0", "end") + text = re.sub(self.ARROW, "->", text) + text = re.sub("\t", " ", text) + lines = text.split("\n") + + # Convert each line to a CFG production + for line in lines: + line = line.strip() + if line == "": + continue + productions += _read_cfg_production(line) + # if line.strip() == '': continue + # if not CFGEditor._PRODUCTION_RE.match(line): + # raise ValueError('Bad production string %r' % line) + # + # (lhs_str, rhs_str) = line.split('->') + # lhs = Nonterminal(lhs_str.strip()) + # rhs = [] + # def parse_token(match, rhs=rhs): + # token = match.group() + # if token[0] in "'\"": rhs.append(token[1:-1]) + # else: rhs.append(Nonterminal(token)) + # return '' + # CFGEditor._TOKEN_RE.sub(parse_token, rhs_str) + # + # productions.append(Production(lhs, *rhs)) + + return productions + + def _destroy(self, *e): + if self._top is None: + return + self._top.destroy() + self._top = None + + def _ok(self, *e): + self._apply() + self._destroy() + + def _apply(self, *e): + productions = self._parse_productions() + start = Nonterminal(self._start.get()) + cfg = CFG(start, productions) + if self._set_cfg_callback is not None: + self._set_cfg_callback(cfg) + + def _reset(self, *e): + self._textwidget.delete("1.0", "end") + for production in self._cfg.productions(): + self._textwidget.insert("end", "%s\n" % production) + self._analyze() + if self._set_cfg_callback is not None: + self._set_cfg_callback(self._cfg) + + def _cancel(self, *e): + try: + self._reset() + except Exception: + pass + self._destroy() + + def _help(self, *e): + # The default font's not very legible; try using 'fixed' instead. + try: + ShowText( + self._parent, + "Help: Chart Parser Demo", + (_CFGEditor_HELP).strip(), + width=75, + font="fixed", + ) + except Exception: + ShowText( + self._parent, + "Help: Chart Parser Demo", + (_CFGEditor_HELP).strip(), + width=75, + ) + + +###################################################################### +# New Demo (built tree based on cfg) +###################################################################### + + +class CFGDemo: + def __init__(self, grammar, text): + self._grammar = grammar + self._text = text + + # Set up the main window. + self._top = Tk() + self._top.title("Context Free Grammar Demo") + + # Base font size + self._size = IntVar(self._top) + self._size.set(12) # = medium + + # Set up the key bindings + self._init_bindings(self._top) + + # Create the basic frames + frame1 = Frame(self._top) + frame1.pack(side="left", fill="y", expand=0) + self._init_menubar(self._top) + self._init_buttons(self._top) + self._init_grammar(frame1) + self._init_treelet(frame1) + self._init_workspace(self._top) + + # ////////////////////////////////////////////////// + # Initialization + # ////////////////////////////////////////////////// + + def _init_bindings(self, top): + top.bind("", self.destroy) + + def _init_menubar(self, parent): + pass + + def _init_buttons(self, parent): + pass + + def _init_grammar(self, parent): + self._prodlist = ProductionList(parent, self._grammar, width=20) + self._prodlist.pack(side="top", fill="both", expand=1) + self._prodlist.focus() + self._prodlist.add_callback("select", self._selectprod_cb) + self._prodlist.add_callback("move", self._selectprod_cb) + + def _init_treelet(self, parent): + self._treelet_canvas = Canvas(parent, background="white") + self._treelet_canvas.pack(side="bottom", fill="x") + self._treelet = None + + def _init_workspace(self, parent): + self._workspace = CanvasFrame(parent, background="white") + self._workspace.pack(side="right", fill="both", expand=1) + self._tree = None + self.reset_workspace() + + # ////////////////////////////////////////////////// + # Workspace + # ////////////////////////////////////////////////// + + def reset_workspace(self): + c = self._workspace.canvas() + fontsize = int(self._size.get()) + node_font = ("helvetica", -(fontsize + 4), "bold") + leaf_font = ("helvetica", -(fontsize + 2)) + + # Remove the old tree + if self._tree is not None: + self._workspace.remove_widget(self._tree) + + # The root of the tree. + start = self._grammar.start().symbol() + rootnode = TextWidget(c, start, font=node_font, draggable=1) + + # The leaves of the tree. + leaves = [] + for word in self._text: + leaves.append(TextWidget(c, word, font=leaf_font, draggable=1)) + + # Put it all together into one tree + self._tree = TreeSegmentWidget(c, rootnode, leaves, color="white") + + # Add it to the workspace. + self._workspace.add_widget(self._tree) + + # Move the leaves to the bottom of the workspace. + for leaf in leaves: + leaf.move(0, 100) + + # self._nodes = {start:1} + # self._leaves = dict([(l,1) for l in leaves]) + + def workspace_markprod(self, production): + pass + + def _markproduction(self, prod, tree=None): + if tree is None: + tree = self._tree + for i in range(len(tree.subtrees()) - len(prod.rhs())): + if tree["color", i] == "white": + self._markproduction # FIXME: Is this necessary at all? + + for j, node in enumerate(prod.rhs()): + widget = tree.subtrees()[i + j] + if ( + isinstance(node, Nonterminal) + and isinstance(widget, TreeSegmentWidget) + and node.symbol == widget.label().text() + ): + pass # matching nonterminal + elif ( + isinstance(node, str) + and isinstance(widget, TextWidget) + and node == widget.text() + ): + pass # matching nonterminal + else: + break + else: + # Everything matched! + print("MATCH AT", i) + + # ////////////////////////////////////////////////// + # Grammar + # ////////////////////////////////////////////////// + + def _selectprod_cb(self, production): + canvas = self._treelet_canvas + + self._prodlist.highlight(production) + if self._treelet is not None: + self._treelet.destroy() + + # Convert the production to a tree. + rhs = production.rhs() + for i, elt in enumerate(rhs): + if isinstance(elt, Nonterminal): + elt = Tree(elt) + tree = Tree(production.lhs().symbol(), *rhs) + + # Draw the tree in the treelet area. + fontsize = int(self._size.get()) + node_font = ("helvetica", -(fontsize + 4), "bold") + leaf_font = ("helvetica", -(fontsize + 2)) + self._treelet = tree_to_treesegment( + canvas, tree, node_font=node_font, leaf_font=leaf_font + ) + self._treelet["draggable"] = 1 + + # Center the treelet. + (x1, y1, x2, y2) = self._treelet.bbox() + w, h = int(canvas["width"]), int(canvas["height"]) + self._treelet.move((w - x1 - x2) / 2, (h - y1 - y2) / 2) + + # Mark the places where we can add it to the workspace. + self._markproduction(production) + + def destroy(self, *args): + self._top.destroy() + + def mainloop(self, *args, **kwargs): + self._top.mainloop(*args, **kwargs) + + +def demo2(): + from nltk import CFG, Nonterminal, Production + + nonterminals = "S VP NP PP P N Name V Det" + (S, VP, NP, PP, P, N, Name, V, Det) = (Nonterminal(s) for s in nonterminals.split()) + productions = ( + # Syntactic Productions + Production(S, [NP, VP]), + Production(NP, [Det, N]), + Production(NP, [NP, PP]), + Production(VP, [VP, PP]), + Production(VP, [V, NP, PP]), + Production(VP, [V, NP]), + Production(PP, [P, NP]), + Production(PP, []), + Production(PP, ["up", "over", NP]), + # Lexical Productions + Production(NP, ["I"]), + Production(Det, ["the"]), + Production(Det, ["a"]), + Production(N, ["man"]), + Production(V, ["saw"]), + Production(P, ["in"]), + Production(P, ["with"]), + Production(N, ["park"]), + Production(N, ["dog"]), + Production(N, ["statue"]), + Production(Det, ["my"]), + ) + grammar = CFG(S, productions) + + text = "I saw a man in the park".split() + d = CFGDemo(grammar, text) + d.mainloop() + + +###################################################################### +# Old Demo +###################################################################### + + +def demo(): + from nltk import CFG, Nonterminal + + nonterminals = "S VP NP PP P N Name V Det" + (S, VP, NP, PP, P, N, Name, V, Det) = (Nonterminal(s) for s in nonterminals.split()) + + grammar = CFG.fromstring( + """ + S -> NP VP + PP -> P NP + NP -> Det N + NP -> NP PP + VP -> V NP + VP -> VP PP + Det -> 'a' + Det -> 'the' + Det -> 'my' + NP -> 'I' + N -> 'dog' + N -> 'man' + N -> 'park' + N -> 'statue' + V -> 'saw' + P -> 'in' + P -> 'up' + P -> 'over' + P -> 'with' + """ + ) + + def cb(grammar): + print(grammar) + + top = Tk() + editor = CFGEditor(top, grammar, cb) + Label(top, text="\nTesting CFG Editor\n").pack() + Button(top, text="Quit", command=top.destroy).pack() + top.mainloop() + + +def demo3(): + from nltk import Production + + (S, VP, NP, PP, P, N, Name, V, Det) = nonterminals( + "S, VP, NP, PP, P, N, Name, V, Det" + ) + + productions = ( + # Syntactic Productions + Production(S, [NP, VP]), + Production(NP, [Det, N]), + Production(NP, [NP, PP]), + Production(VP, [VP, PP]), + Production(VP, [V, NP, PP]), + Production(VP, [V, NP]), + Production(PP, [P, NP]), + Production(PP, []), + Production(PP, ["up", "over", NP]), + # Lexical Productions + Production(NP, ["I"]), + Production(Det, ["the"]), + Production(Det, ["a"]), + Production(N, ["man"]), + Production(V, ["saw"]), + Production(P, ["in"]), + Production(P, ["with"]), + Production(N, ["park"]), + Production(N, ["dog"]), + Production(N, ["statue"]), + Production(Det, ["my"]), + ) + + t = Tk() + + def destroy(e, t=t): + t.destroy() + + t.bind("q", destroy) + p = ProductionList(t, productions) + p.pack(expand=1, fill="both") + p.add_callback("select", p.markonly) + p.add_callback("move", p.markonly) + p.focus() + p.mark(productions[2]) + p.mark(productions[8]) + + +if __name__ == "__main__": + demo() diff --git a/nltk/draw/dispersion.py b/nltk/draw/dispersion.py new file mode 100644 index 0000000..939c262 --- /dev/null +++ b/nltk/draw/dispersion.py @@ -0,0 +1,67 @@ +# Natural Language Toolkit: Dispersion Plots +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +A utility for displaying lexical dispersion. +""" + + +def dispersion_plot(text, words, ignore_case=False, title="Lexical Dispersion Plot"): + """ + Generate a lexical dispersion plot. + + :param text: The source text + :type text: list(str) or iter(str) + :param words: The target words + :type words: list of str + :param ignore_case: flag to set if case should be ignored when searching text + :type ignore_case: bool + :return: a matplotlib Axes object that may still be modified before plotting + :rtype: Axes + """ + + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "The plot function requires matplotlib to be installed. " + "See https://matplotlib.org/" + ) from e + + word2y = { + word.casefold() if ignore_case else word: y + for y, word in enumerate(reversed(words)) + } + xs, ys = [], [] + for x, token in enumerate(text): + token = token.casefold() if ignore_case else token + y = word2y.get(token) + if y is not None: + xs.append(x) + ys.append(y) + + words = words[::-1] + + _, ax = plt.subplots() + ax.plot(xs, ys, "|") + ax.dataLim.x0, ax.dataLim.x1 = 0, len(text) - 1 + ax.autoscale(axis="x") + ax.set_yticks(list(range(len(words))), words, color="C0") + ax.set_ylim(-1, len(words)) + ax.set_title(title) + ax.set_xlabel("Word Offset") + return ax + + +if __name__ == "__main__": + import matplotlib.pyplot as plt + + from nltk.corpus import gutenberg + + words = ["Elinor", "Marianne", "Edward", "Willoughby"] + dispersion_plot(gutenberg.words("austen-sense.txt"), words) + plt.show() diff --git a/nltk/draw/table.py b/nltk/draw/table.py new file mode 100644 index 0000000..7f9b097 --- /dev/null +++ b/nltk/draw/table.py @@ -0,0 +1,1178 @@ +# Natural Language Toolkit: Table widget +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Tkinter widgets for displaying multi-column listboxes and tables. +""" + +import operator +from tkinter import Frame, Label, Listbox, Scrollbar, Tk + +###################################################################### +# Multi-Column Listbox +###################################################################### + + +class MultiListbox(Frame): + """ + A multi-column listbox, where the current selection applies to an + entire row. Based on the MultiListbox Tkinter widget + recipe from the Python Cookbook (https://code.activestate.com/recipes/52266/) + + For the most part, ``MultiListbox`` methods delegate to its + contained listboxes. For any methods that do not have docstrings, + see ``Tkinter.Listbox`` for a description of what that method does. + """ + + # ///////////////////////////////////////////////////////////////// + # Configuration + # ///////////////////////////////////////////////////////////////// + + #: Default configuration values for the frame. + FRAME_CONFIG = dict(background="#888", takefocus=True, highlightthickness=1) + + #: Default configurations for the column labels. + LABEL_CONFIG = dict( + borderwidth=1, + relief="raised", + font="helvetica -16 bold", + background="#444", + foreground="white", + ) + + #: Default configuration for the column listboxes. + LISTBOX_CONFIG = dict( + borderwidth=1, + selectborderwidth=0, + highlightthickness=0, + exportselection=False, + selectbackground="#888", + activestyle="none", + takefocus=False, + ) + + # ///////////////////////////////////////////////////////////////// + # Constructor + # ///////////////////////////////////////////////////////////////// + + def __init__(self, master, columns, column_weights=None, cnf={}, **kw): + """ + Construct a new multi-column listbox widget. + + :param master: The widget that should contain the new + multi-column listbox. + + :param columns: Specifies what columns should be included in + the new multi-column listbox. If ``columns`` is an integer, + then it is the number of columns to include. If it is + a list, then its length indicates the number of columns + to include; and each element of the list will be used as + a label for the corresponding column. + + :param cnf, kw: Configuration parameters for this widget. + Use ``label_*`` to configure all labels; and ``listbox_*`` + to configure all listboxes. E.g.: + >>> root = Tk() # doctest: +SKIP + >>> MultiListbox(root, ["Subject", "Sender", "Date"], label_foreground='red').pack() # doctest: +SKIP + """ + # If columns was specified as an int, convert it to a list. + if isinstance(columns, int): + columns = list(range(columns)) + include_labels = False + else: + include_labels = True + + if len(columns) == 0: + raise ValueError("Expected at least one column") + + # Instance variables + self._column_names = tuple(columns) + self._listboxes = [] + self._labels = [] + + # Pick a default value for column_weights, if none was specified. + if column_weights is None: + column_weights = [1] * len(columns) + elif len(column_weights) != len(columns): + raise ValueError("Expected one column_weight for each column") + self._column_weights = column_weights + + # Configure our widgets. + Frame.__init__(self, master, **self.FRAME_CONFIG) + self.grid_rowconfigure(1, weight=1) + for i, label in enumerate(self._column_names): + self.grid_columnconfigure(i, weight=column_weights[i]) + + # Create a label for the column + if include_labels: + l = Label(self, text=label, **self.LABEL_CONFIG) + self._labels.append(l) + l.grid(column=i, row=0, sticky="news", padx=0, pady=0) + l.column_index = i + + # Create a listbox for the column + lb = Listbox(self, **self.LISTBOX_CONFIG) + self._listboxes.append(lb) + lb.grid(column=i, row=1, sticky="news", padx=0, pady=0) + lb.column_index = i + + # Clicking or dragging selects: + lb.bind("", self._select) + lb.bind("", self._select) + # Scroll wheel scrolls: + lb.bind("", lambda e: self._scroll(-1)) + lb.bind("", lambda e: self._scroll(+1)) + lb.bind("", lambda e: self._scroll(e.delta)) + # Button 2 can be used to scan: + lb.bind("", lambda e: self.scan_mark(e.x, e.y)) + lb.bind("", lambda e: self.scan_dragto(e.x, e.y)) + # Dragging outside the window has no effect (disable + # the default listbox behavior, which scrolls): + lb.bind("", lambda e: "break") + # Columns can be resized by dragging them: + lb.bind("", self._resize_column) + + # Columns can be resized by dragging them. (This binding is + # used if they click on the grid between columns:) + self.bind("", self._resize_column) + + # Set up key bindings for the widget: + self.bind("", lambda e: self.select(delta=-1)) + self.bind("", lambda e: self.select(delta=1)) + self.bind("", lambda e: self.select(delta=-self._pagesize())) + self.bind("", lambda e: self.select(delta=self._pagesize())) + + # Configuration customizations + self.configure(cnf, **kw) + + # ///////////////////////////////////////////////////////////////// + # Column Resizing + # ///////////////////////////////////////////////////////////////// + + def _resize_column(self, event): + """ + Callback used to resize a column of the table. Return ``True`` + if the column is actually getting resized (if the user clicked + on the far left or far right 5 pixels of a label); and + ``False`` otherwies. + """ + # If we're already waiting for a button release, then ignore + # the new button press. + if event.widget.bind(""): + return False + + # Decide which column (if any) to resize. + self._resize_column_index = None + if event.widget is self: + for i, lb in enumerate(self._listboxes): + if abs(event.x - (lb.winfo_x() + lb.winfo_width())) < 10: + self._resize_column_index = i + elif event.x > (event.widget.winfo_width() - 5): + self._resize_column_index = event.widget.column_index + elif event.x < 5 and event.widget.column_index != 0: + self._resize_column_index = event.widget.column_index - 1 + + # Bind callbacks that are used to resize it. + if self._resize_column_index is not None: + event.widget.bind("", self._resize_column_motion_cb) + event.widget.bind( + "" % event.num, self._resize_column_buttonrelease_cb + ) + return True + else: + return False + + def _resize_column_motion_cb(self, event): + lb = self._listboxes[self._resize_column_index] + charwidth = lb.winfo_width() / lb["width"] + + x1 = event.x + event.widget.winfo_x() + x2 = lb.winfo_x() + lb.winfo_width() + + lb["width"] = max(3, int(lb["width"] + (x1 - x2) // charwidth)) + + def _resize_column_buttonrelease_cb(self, event): + event.widget.unbind("" % event.num) + event.widget.unbind("") + + # ///////////////////////////////////////////////////////////////// + # Properties + # ///////////////////////////////////////////////////////////////// + + @property + def column_names(self): + """ + A tuple containing the names of the columns used by this + multi-column listbox. + """ + return self._column_names + + @property + def column_labels(self): + """ + A tuple containing the ``Tkinter.Label`` widgets used to + display the label of each column. If this multi-column + listbox was created without labels, then this will be an empty + tuple. These widgets will all be augmented with a + ``column_index`` attribute, which can be used to determine + which column they correspond to. This can be convenient, + e.g., when defining callbacks for bound events. + """ + return tuple(self._labels) + + @property + def listboxes(self): + """ + A tuple containing the ``Tkinter.Listbox`` widgets used to + display individual columns. These widgets will all be + augmented with a ``column_index`` attribute, which can be used + to determine which column they correspond to. This can be + convenient, e.g., when defining callbacks for bound events. + """ + return tuple(self._listboxes) + + # ///////////////////////////////////////////////////////////////// + # Mouse & Keyboard Callback Functions + # ///////////////////////////////////////////////////////////////// + + def _select(self, e): + i = e.widget.nearest(e.y) + self.selection_clear(0, "end") + self.selection_set(i) + self.activate(i) + self.focus() + + def _scroll(self, delta): + for lb in self._listboxes: + lb.yview_scroll(delta, "unit") + return "break" + + def _pagesize(self): + """:return: The number of rows that makes up one page""" + return int(self.index("@0,1000000")) - int(self.index("@0,0")) + + # ///////////////////////////////////////////////////////////////// + # Row selection + # ///////////////////////////////////////////////////////////////// + + def select(self, index=None, delta=None, see=True): + """ + Set the selected row. If ``index`` is specified, then select + row ``index``. Otherwise, if ``delta`` is specified, then move + the current selection by ``delta`` (negative numbers for up, + positive numbers for down). This will not move the selection + past the top or the bottom of the list. + + :param see: If true, then call ``self.see()`` with the newly + selected index, to ensure that it is visible. + """ + if (index is not None) and (delta is not None): + raise ValueError("specify index or delta, but not both") + + # If delta was given, then calculate index. + if delta is not None: + if len(self.curselection()) == 0: + index = -1 + delta + else: + index = int(self.curselection()[0]) + delta + + # Clear all selected rows. + self.selection_clear(0, "end") + + # Select the specified index + if index is not None: + index = min(max(index, 0), self.size() - 1) + # self.activate(index) + self.selection_set(index) + if see: + self.see(index) + + # ///////////////////////////////////////////////////////////////// + # Configuration + # ///////////////////////////////////////////////////////////////// + + def configure(self, cnf={}, **kw): + """ + Configure this widget. Use ``label_*`` to configure all + labels; and ``listbox_*`` to configure all listboxes. E.g.: + + >>> master = Tk() # doctest: +SKIP + >>> mlb = MultiListbox(master, 5) # doctest: +SKIP + >>> mlb.configure(label_foreground='red') # doctest: +SKIP + >>> mlb.configure(listbox_foreground='red') # doctest: +SKIP + """ + cnf = dict(list(cnf.items()) + list(kw.items())) + for key, val in list(cnf.items()): + if key.startswith("label_") or key.startswith("label-"): + for label in self._labels: + label.configure({key[6:]: val}) + elif key.startswith("listbox_") or key.startswith("listbox-"): + for listbox in self._listboxes: + listbox.configure({key[8:]: val}) + else: + Frame.configure(self, {key: val}) + + def __setitem__(self, key, val): + """ + Configure this widget. This is equivalent to + ``self.configure({key,val``)}. See ``configure()``. + """ + self.configure({key: val}) + + def rowconfigure(self, row_index, cnf={}, **kw): + """ + Configure all table cells in the given row. Valid keyword + arguments are: ``background``, ``bg``, ``foreground``, ``fg``, + ``selectbackground``, ``selectforeground``. + """ + for lb in self._listboxes: + lb.itemconfigure(row_index, cnf, **kw) + + def columnconfigure(self, col_index, cnf={}, **kw): + """ + Configure all table cells in the given column. Valid keyword + arguments are: ``background``, ``bg``, ``foreground``, ``fg``, + ``selectbackground``, ``selectforeground``. + """ + lb = self._listboxes[col_index] + + cnf = dict(list(cnf.items()) + list(kw.items())) + for key, val in list(cnf.items()): + if key in ( + "background", + "bg", + "foreground", + "fg", + "selectbackground", + "selectforeground", + ): + for i in range(lb.size()): + lb.itemconfigure(i, {key: val}) + else: + lb.configure({key: val}) + + def itemconfigure(self, row_index, col_index, cnf=None, **kw): + """ + Configure the table cell at the given row and column. Valid + keyword arguments are: ``background``, ``bg``, ``foreground``, + ``fg``, ``selectbackground``, ``selectforeground``. + """ + lb = self._listboxes[col_index] + return lb.itemconfigure(row_index, cnf, **kw) + + # ///////////////////////////////////////////////////////////////// + # Value Access + # ///////////////////////////////////////////////////////////////// + + def insert(self, index, *rows): + """ + Insert the given row or rows into the table, at the given + index. Each row value should be a tuple of cell values, one + for each column in the row. Index may be an integer or any of + the special strings (such as ``'end'``) accepted by + ``Tkinter.Listbox``. + """ + for elt in rows: + if len(elt) != len(self._column_names): + raise ValueError( + "rows should be tuples whose length " + "is equal to the number of columns" + ) + for lb, elts in zip(self._listboxes, list(zip(*rows))): + lb.insert(index, *elts) + + def get(self, first, last=None): + """ + Return the value(s) of the specified row(s). If ``last`` is + not specified, then return a single row value; otherwise, + return a list of row values. Each row value is a tuple of + cell values, one for each column in the row. + """ + values = [lb.get(first, last) for lb in self._listboxes] + if last: + return [tuple(row) for row in zip(*values)] + else: + return tuple(values) + + def bbox(self, row, col): + """ + Return the bounding box for the given table cell, relative to + this widget's top-left corner. The bounding box is a tuple + of integers ``(left, top, width, height)``. + """ + dx, dy, _, _ = self.grid_bbox(row=0, column=col) + x, y, w, h = self._listboxes[col].bbox(row) + return int(x) + int(dx), int(y) + int(dy), int(w), int(h) + + # ///////////////////////////////////////////////////////////////// + # Hide/Show Columns + # ///////////////////////////////////////////////////////////////// + + def hide_column(self, col_index): + """ + Hide the given column. The column's state is still + maintained: its values will still be returned by ``get()``, and + you must supply its values when calling ``insert()``. It is + safe to call this on a column that is already hidden. + + :see: ``show_column()`` + """ + if self._labels: + self._labels[col_index].grid_forget() + self.listboxes[col_index].grid_forget() + self.grid_columnconfigure(col_index, weight=0) + + def show_column(self, col_index): + """ + Display a column that has been hidden using ``hide_column()``. + It is safe to call this on a column that is not hidden. + """ + weight = self._column_weights[col_index] + if self._labels: + self._labels[col_index].grid( + column=col_index, row=0, sticky="news", padx=0, pady=0 + ) + self._listboxes[col_index].grid( + column=col_index, row=1, sticky="news", padx=0, pady=0 + ) + self.grid_columnconfigure(col_index, weight=weight) + + # ///////////////////////////////////////////////////////////////// + # Binding Methods + # ///////////////////////////////////////////////////////////////// + + def bind_to_labels(self, sequence=None, func=None, add=None): + """ + Add a binding to each ``Tkinter.Label`` widget in this + mult-column listbox that will call ``func`` in response to the + event sequence. + + :return: A list of the identifiers of replaced binding + functions (if any), allowing for their deletion (to + prevent a memory leak). + """ + return [label.bind(sequence, func, add) for label in self.column_labels] + + def bind_to_listboxes(self, sequence=None, func=None, add=None): + """ + Add a binding to each ``Tkinter.Listbox`` widget in this + mult-column listbox that will call ``func`` in response to the + event sequence. + + :return: A list of the identifiers of replaced binding + functions (if any), allowing for their deletion (to + prevent a memory leak). + """ + for listbox in self.listboxes: + listbox.bind(sequence, func, add) + + def bind_to_columns(self, sequence=None, func=None, add=None): + """ + Add a binding to each ``Tkinter.Label`` and ``Tkinter.Listbox`` + widget in this mult-column listbox that will call ``func`` in + response to the event sequence. + + :return: A list of the identifiers of replaced binding + functions (if any), allowing for their deletion (to + prevent a memory leak). + """ + return self.bind_to_labels(sequence, func, add) + self.bind_to_listboxes( + sequence, func, add + ) + + # ///////////////////////////////////////////////////////////////// + # Simple Delegation + # ///////////////////////////////////////////////////////////////// + + # These methods delegate to the first listbox: + def curselection(self, *args, **kwargs): + return self._listboxes[0].curselection(*args, **kwargs) + + def selection_includes(self, *args, **kwargs): + return self._listboxes[0].selection_includes(*args, **kwargs) + + def itemcget(self, *args, **kwargs): + return self._listboxes[0].itemcget(*args, **kwargs) + + def size(self, *args, **kwargs): + return self._listboxes[0].size(*args, **kwargs) + + def index(self, *args, **kwargs): + return self._listboxes[0].index(*args, **kwargs) + + def nearest(self, *args, **kwargs): + return self._listboxes[0].nearest(*args, **kwargs) + + # These methods delegate to each listbox (and return None): + def activate(self, *args, **kwargs): + for lb in self._listboxes: + lb.activate(*args, **kwargs) + + def delete(self, *args, **kwargs): + for lb in self._listboxes: + lb.delete(*args, **kwargs) + + def scan_mark(self, *args, **kwargs): + for lb in self._listboxes: + lb.scan_mark(*args, **kwargs) + + def scan_dragto(self, *args, **kwargs): + for lb in self._listboxes: + lb.scan_dragto(*args, **kwargs) + + def see(self, *args, **kwargs): + for lb in self._listboxes: + lb.see(*args, **kwargs) + + def selection_anchor(self, *args, **kwargs): + for lb in self._listboxes: + lb.selection_anchor(*args, **kwargs) + + def selection_clear(self, *args, **kwargs): + for lb in self._listboxes: + lb.selection_clear(*args, **kwargs) + + def selection_set(self, *args, **kwargs): + for lb in self._listboxes: + lb.selection_set(*args, **kwargs) + + def yview(self, *args, **kwargs): + for lb in self._listboxes: + v = lb.yview(*args, **kwargs) + return v # if called with no arguments + + def yview_moveto(self, *args, **kwargs): + for lb in self._listboxes: + lb.yview_moveto(*args, **kwargs) + + def yview_scroll(self, *args, **kwargs): + for lb in self._listboxes: + lb.yview_scroll(*args, **kwargs) + + # ///////////////////////////////////////////////////////////////// + # Aliases + # ///////////////////////////////////////////////////////////////// + + itemconfig = itemconfigure + rowconfig = rowconfigure + columnconfig = columnconfigure + select_anchor = selection_anchor + select_clear = selection_clear + select_includes = selection_includes + select_set = selection_set + + # ///////////////////////////////////////////////////////////////// + # These listbox methods are not defined for multi-listbox + # ///////////////////////////////////////////////////////////////// + # def xview(self, *what): pass + # def xview_moveto(self, fraction): pass + # def xview_scroll(self, number, what): pass + + +###################################################################### +# Table +###################################################################### + + +class Table: + """ + A display widget for a table of values, based on a ``MultiListbox`` + widget. For many purposes, ``Table`` can be treated as a + list-of-lists. E.g., table[i] is a list of the values for row i; + and table.append(row) adds a new row with the given list of + values. Individual cells can be accessed using table[i,j], which + refers to the j-th column of the i-th row. This can be used to + both read and write values from the table. E.g.: + + >>> table[i,j] = 'hello' # doctest: +SKIP + + The column (j) can be given either as an index number, or as a + column name. E.g., the following prints the value in the 3rd row + for the 'First Name' column: + + >>> print(table[3, 'First Name']) # doctest: +SKIP + John + + You can configure the colors for individual rows, columns, or + cells using ``rowconfig()``, ``columnconfig()``, and ``itemconfig()``. + The color configuration for each row will be preserved if the + table is modified; however, when new rows are added, any color + configurations that have been made for *columns* will not be + applied to the new row. + + Note: Although ``Table`` acts like a widget in some ways (e.g., it + defines ``grid()``, ``pack()``, and ``bind()``), it is not itself a + widget; it just contains one. This is because widgets need to + define ``__getitem__()``, ``__setitem__()``, and ``__nonzero__()`` in + a way that's incompatible with the fact that ``Table`` behaves as a + list-of-lists. + + :ivar _mlb: The multi-column listbox used to display this table's data. + :ivar _rows: A list-of-lists used to hold the cell values of this + table. Each element of _rows is a row value, i.e., a list of + cell values, one for each column in the row. + """ + + def __init__( + self, + master, + column_names, + rows=None, + column_weights=None, + scrollbar=True, + click_to_sort=True, + reprfunc=None, + cnf={}, + **kw + ): + """ + Construct a new Table widget. + + :type master: Tkinter.Widget + :param master: The widget that should contain the new table. + :type column_names: list(str) + :param column_names: A list of names for the columns; these + names will be used to create labels for each column; + and can be used as an index when reading or writing + cell values from the table. + :type rows: list(list) + :param rows: A list of row values used to initialize the table. + Each row value should be a tuple of cell values, one for + each column in the row. + :type scrollbar: bool + :param scrollbar: If true, then create a scrollbar for the + new table widget. + :type click_to_sort: bool + :param click_to_sort: If true, then create bindings that will + sort the table's rows by a given column's values if the + user clicks on that colum's label. + :type reprfunc: function + :param reprfunc: If specified, then use this function to + convert each table cell value to a string suitable for + display. ``reprfunc`` has the following signature: + reprfunc(row_index, col_index, cell_value) -> str + (Note that the column is specified by index, not by name.) + :param cnf, kw: Configuration parameters for this widget's + contained ``MultiListbox``. See ``MultiListbox.__init__()`` + for details. + """ + self._num_columns = len(column_names) + self._reprfunc = reprfunc + self._frame = Frame(master) + + self._column_name_to_index = {c: i for (i, c) in enumerate(column_names)} + + # Make a copy of the rows & check that it's valid. + if rows is None: + self._rows = [] + else: + self._rows = [[v for v in row] for row in rows] + for row in self._rows: + self._checkrow(row) + + # Create our multi-list box. + self._mlb = MultiListbox(self._frame, column_names, column_weights, cnf, **kw) + self._mlb.pack(side="left", expand=True, fill="both") + + # Optional scrollbar + if scrollbar: + sb = Scrollbar(self._frame, orient="vertical", command=self._mlb.yview) + self._mlb.listboxes[0]["yscrollcommand"] = sb.set + # for listbox in self._mlb.listboxes: + # listbox['yscrollcommand'] = sb.set + sb.pack(side="right", fill="y") + self._scrollbar = sb + + # Set up sorting + self._sortkey = None + if click_to_sort: + for i, l in enumerate(self._mlb.column_labels): + l.bind("", self._sort) + + # Fill in our multi-list box. + self._fill_table() + + # ///////////////////////////////////////////////////////////////// + # { Widget-like Methods + # ///////////////////////////////////////////////////////////////// + # These all just delegate to either our frame or our MLB. + + def pack(self, *args, **kwargs): + """Position this table's main frame widget in its parent + widget. See ``Tkinter.Frame.pack()`` for more info.""" + self._frame.pack(*args, **kwargs) + + def grid(self, *args, **kwargs): + """Position this table's main frame widget in its parent + widget. See ``Tkinter.Frame.grid()`` for more info.""" + self._frame.grid(*args, **kwargs) + + def focus(self): + """Direct (keyboard) input foxus to this widget.""" + self._mlb.focus() + + def bind(self, sequence=None, func=None, add=None): + """Add a binding to this table's main frame that will call + ``func`` in response to the event sequence.""" + self._mlb.bind(sequence, func, add) + + def rowconfigure(self, row_index, cnf={}, **kw): + """:see: ``MultiListbox.rowconfigure()``""" + self._mlb.rowconfigure(row_index, cnf, **kw) + + def columnconfigure(self, col_index, cnf={}, **kw): + """:see: ``MultiListbox.columnconfigure()``""" + col_index = self.column_index(col_index) + self._mlb.columnconfigure(col_index, cnf, **kw) + + def itemconfigure(self, row_index, col_index, cnf=None, **kw): + """:see: ``MultiListbox.itemconfigure()``""" + col_index = self.column_index(col_index) + return self._mlb.itemconfigure(row_index, col_index, cnf, **kw) + + def bind_to_labels(self, sequence=None, func=None, add=None): + """:see: ``MultiListbox.bind_to_labels()``""" + return self._mlb.bind_to_labels(sequence, func, add) + + def bind_to_listboxes(self, sequence=None, func=None, add=None): + """:see: ``MultiListbox.bind_to_listboxes()``""" + return self._mlb.bind_to_listboxes(sequence, func, add) + + def bind_to_columns(self, sequence=None, func=None, add=None): + """:see: ``MultiListbox.bind_to_columns()``""" + return self._mlb.bind_to_columns(sequence, func, add) + + rowconfig = rowconfigure + columnconfig = columnconfigure + itemconfig = itemconfigure + + # ///////////////////////////////////////////////////////////////// + # { Table as list-of-lists + # ///////////////////////////////////////////////////////////////// + + def insert(self, row_index, rowvalue): + """ + Insert a new row into the table, so that its row index will be + ``row_index``. If the table contains any rows whose row index + is greater than or equal to ``row_index``, then they will be + shifted down. + + :param rowvalue: A tuple of cell values, one for each column + in the new row. + """ + self._checkrow(rowvalue) + self._rows.insert(row_index, rowvalue) + if self._reprfunc is not None: + rowvalue = [ + self._reprfunc(row_index, j, v) for (j, v) in enumerate(rowvalue) + ] + self._mlb.insert(row_index, rowvalue) + if self._DEBUG: + self._check_table_vs_mlb() + + def extend(self, rowvalues): + """ + Add new rows at the end of the table. + + :param rowvalues: A list of row values used to initialize the + table. Each row value should be a tuple of cell values, + one for each column in the row. + """ + for rowvalue in rowvalues: + self.append(rowvalue) + if self._DEBUG: + self._check_table_vs_mlb() + + def append(self, rowvalue): + """ + Add a new row to the end of the table. + + :param rowvalue: A tuple of cell values, one for each column + in the new row. + """ + self.insert(len(self._rows), rowvalue) + if self._DEBUG: + self._check_table_vs_mlb() + + def clear(self): + """ + Delete all rows in this table. + """ + self._rows = [] + self._mlb.delete(0, "end") + if self._DEBUG: + self._check_table_vs_mlb() + + def __getitem__(self, index): + """ + Return the value of a row or a cell in this table. If + ``index`` is an integer, then the row value for the ``index``th + row. This row value consists of a tuple of cell values, one + for each column in the row. If ``index`` is a tuple of two + integers, ``(i,j)``, then return the value of the cell in the + ``i``th row and the ``j``th column. + """ + if isinstance(index, slice): + raise ValueError("Slicing not supported") + elif isinstance(index, tuple) and len(index) == 2: + return self._rows[index[0]][self.column_index(index[1])] + else: + return tuple(self._rows[index]) + + def __setitem__(self, index, val): + """ + Replace the value of a row or a cell in this table with + ``val``. + + If ``index`` is an integer, then ``val`` should be a row value + (i.e., a tuple of cell values, one for each column). In this + case, the values of the ``index``th row of the table will be + replaced with the values in ``val``. + + If ``index`` is a tuple of integers, ``(i,j)``, then replace the + value of the cell in the ``i``th row and ``j``th column with + ``val``. + """ + if isinstance(index, slice): + raise ValueError("Slicing not supported") + + # table[i,j] = val + elif isinstance(index, tuple) and len(index) == 2: + i, j = index[0], self.column_index(index[1]) + config_cookie = self._save_config_info([i]) + self._rows[i][j] = val + if self._reprfunc is not None: + val = self._reprfunc(i, j, val) + self._mlb.listboxes[j].insert(i, val) + self._mlb.listboxes[j].delete(i + 1) + self._restore_config_info(config_cookie) + + # table[i] = val + else: + config_cookie = self._save_config_info([index]) + self._checkrow(val) + self._rows[index] = list(val) + if self._reprfunc is not None: + val = [self._reprfunc(index, j, v) for (j, v) in enumerate(val)] + self._mlb.insert(index, val) + self._mlb.delete(index + 1) + self._restore_config_info(config_cookie) + + def __delitem__(self, row_index): + """ + Delete the ``row_index``th row from this table. + """ + if isinstance(row_index, slice): + raise ValueError("Slicing not supported") + if isinstance(row_index, tuple) and len(row_index) == 2: + raise ValueError("Cannot delete a single cell!") + del self._rows[row_index] + self._mlb.delete(row_index) + if self._DEBUG: + self._check_table_vs_mlb() + + def __len__(self): + """ + :return: the number of rows in this table. + """ + return len(self._rows) + + def _checkrow(self, rowvalue): + """ + Helper function: check that a given row value has the correct + number of elements; and if not, raise an exception. + """ + if len(rowvalue) != self._num_columns: + raise ValueError( + "Row %r has %d columns; expected %d" + % (rowvalue, len(rowvalue), self._num_columns) + ) + + # ///////////////////////////////////////////////////////////////// + # Columns + # ///////////////////////////////////////////////////////////////// + + @property + def column_names(self): + """A list of the names of the columns in this table.""" + return self._mlb.column_names + + def column_index(self, i): + """ + If ``i`` is a valid column index integer, then return it as is. + Otherwise, check if ``i`` is used as the name for any column; + if so, return that column's index. Otherwise, raise a + ``KeyError`` exception. + """ + if isinstance(i, int) and 0 <= i < self._num_columns: + return i + else: + # This raises a key error if the column is not found. + return self._column_name_to_index[i] + + def hide_column(self, column_index): + """:see: ``MultiListbox.hide_column()``""" + self._mlb.hide_column(self.column_index(column_index)) + + def show_column(self, column_index): + """:see: ``MultiListbox.show_column()``""" + self._mlb.show_column(self.column_index(column_index)) + + # ///////////////////////////////////////////////////////////////// + # Selection + # ///////////////////////////////////////////////////////////////// + + def selected_row(self): + """ + Return the index of the currently selected row, or None if + no row is selected. To get the row value itself, use + ``table[table.selected_row()]``. + """ + sel = self._mlb.curselection() + if sel: + return int(sel[0]) + else: + return None + + def select(self, index=None, delta=None, see=True): + """:see: ``MultiListbox.select()``""" + self._mlb.select(index, delta, see) + + # ///////////////////////////////////////////////////////////////// + # Sorting + # ///////////////////////////////////////////////////////////////// + + def sort_by(self, column_index, order="toggle"): + """ + Sort the rows in this table, using the specified column's + values as a sort key. + + :param column_index: Specifies which column to sort, using + either a column index (int) or a column's label name + (str). + + :param order: Specifies whether to sort the values in + ascending or descending order: + + - ``'ascending'``: Sort from least to greatest. + - ``'descending'``: Sort from greatest to least. + - ``'toggle'``: If the most recent call to ``sort_by()`` + sorted the table by the same column (``column_index``), + then reverse the rows; otherwise sort in ascending + order. + """ + if order not in ("ascending", "descending", "toggle"): + raise ValueError( + 'sort_by(): order should be "ascending", ' '"descending", or "toggle".' + ) + column_index = self.column_index(column_index) + config_cookie = self._save_config_info(index_by_id=True) + + # Sort the rows. + if order == "toggle" and column_index == self._sortkey: + self._rows.reverse() + else: + self._rows.sort( + key=operator.itemgetter(column_index), reverse=(order == "descending") + ) + self._sortkey = column_index + + # Redraw the table. + self._fill_table() + self._restore_config_info(config_cookie, index_by_id=True, see=True) + if self._DEBUG: + self._check_table_vs_mlb() + + def _sort(self, event): + """Event handler for clicking on a column label -- sort by + that column.""" + column_index = event.widget.column_index + + # If they click on the far-left of far-right of a column's + # label, then resize rather than sorting. + if self._mlb._resize_column(event): + return "continue" + + # Otherwise, sort. + else: + self.sort_by(column_index) + return "continue" + + # ///////////////////////////////////////////////////////////////// + # { Table Drawing Helpers + # ///////////////////////////////////////////////////////////////// + + def _fill_table(self, save_config=True): + """ + Re-draw the table from scratch, by clearing out the table's + multi-column listbox; and then filling it in with values from + ``self._rows``. Note that any cell-, row-, or column-specific + color configuration that has been done will be lost. The + selection will also be lost -- i.e., no row will be selected + after this call completes. + """ + self._mlb.delete(0, "end") + for i, row in enumerate(self._rows): + if self._reprfunc is not None: + row = [self._reprfunc(i, j, v) for (j, v) in enumerate(row)] + self._mlb.insert("end", row) + + def _get_itemconfig(self, r, c): + return { + k: self._mlb.itemconfig(r, c, k)[-1] + for k in ( + "foreground", + "selectforeground", + "background", + "selectbackground", + ) + } + + def _save_config_info(self, row_indices=None, index_by_id=False): + """ + Return a 'cookie' containing information about which row is + selected, and what color configurations have been applied. + this information can the be re-applied to the table (after + making modifications) using ``_restore_config_info()``. Color + configuration information will be saved for any rows in + ``row_indices``, or in the entire table, if + ``row_indices=None``. If ``index_by_id=True``, the the cookie + will associate rows with their configuration information based + on the rows' python id. This is useful when performing + operations that re-arrange the rows (e.g. ``sort``). If + ``index_by_id=False``, then it is assumed that all rows will be + in the same order when ``_restore_config_info()`` is called. + """ + # Default value for row_indices is all rows. + if row_indices is None: + row_indices = list(range(len(self._rows))) + + # Look up our current selection. + selection = self.selected_row() + if index_by_id and selection is not None: + selection = id(self._rows[selection]) + + # Look up the color configuration info for each row. + if index_by_id: + config = { + id(self._rows[r]): [ + self._get_itemconfig(r, c) for c in range(self._num_columns) + ] + for r in row_indices + } + else: + config = { + r: [self._get_itemconfig(r, c) for c in range(self._num_columns)] + for r in row_indices + } + + return selection, config + + def _restore_config_info(self, cookie, index_by_id=False, see=False): + """ + Restore selection & color configuration information that was + saved using ``_save_config_info``. + """ + selection, config = cookie + + # Clear the selection. + if selection is None: + self._mlb.selection_clear(0, "end") + + # Restore selection & color config + if index_by_id: + for r, row in enumerate(self._rows): + if id(row) in config: + for c in range(self._num_columns): + self._mlb.itemconfigure(r, c, config[id(row)][c]) + if id(row) == selection: + self._mlb.select(r, see=see) + else: + if selection is not None: + self._mlb.select(selection, see=see) + for r in config: + for c in range(self._num_columns): + self._mlb.itemconfigure(r, c, config[r][c]) + + # ///////////////////////////////////////////////////////////////// + # Debugging (Invariant Checker) + # ///////////////////////////////////////////////////////////////// + + _DEBUG = False + """If true, then run ``_check_table_vs_mlb()`` after any operation + that modifies the table.""" + + def _check_table_vs_mlb(self): + """ + Verify that the contents of the table's ``_rows`` variable match + the contents of its multi-listbox (``_mlb``). This is just + included for debugging purposes, to make sure that the + list-modifying operations are working correctly. + """ + for col in self._mlb.listboxes: + assert len(self) == col.size() + for row in self: + assert len(row) == self._num_columns + assert self._num_columns == len(self._mlb.column_names) + # assert self._column_names == self._mlb.column_names + for i, row in enumerate(self): + for j, cell in enumerate(row): + if self._reprfunc is not None: + cell = self._reprfunc(i, j, cell) + assert self._mlb.get(i)[j] == cell + + +###################################################################### +# Demo/Test Function +###################################################################### + + +# update this to use new WordNet API +def demo(): + root = Tk() + root.bind("", lambda e: root.destroy()) + + table = Table( + root, + "Word Synset Hypernym Hyponym".split(), + column_weights=[0, 1, 1, 1], + reprfunc=(lambda i, j, s: " %s" % s), + ) + table.pack(expand=True, fill="both") + + from nltk.corpus import brown, wordnet + + for word, pos in sorted(set(brown.tagged_words()[:500])): + if pos[0] != "N": + continue + word = word.lower() + for synset in wordnet.synsets(word): + try: + hyper_def = synset.hypernyms()[0].definition() + except Exception: + hyper_def = "*none*" + try: + hypo_def = synset.hypernyms()[0].definition() + except Exception: + hypo_def = "*none*" + table.append([word, synset.definition(), hyper_def, hypo_def]) + + table.columnconfig("Word", background="#afa") + table.columnconfig("Synset", background="#efe") + table.columnconfig("Hypernym", background="#fee") + table.columnconfig("Hyponym", background="#ffe") + for row in range(len(table)): + for column in ("Hypernym", "Hyponym"): + if table[row, column] == "*none*": + table.itemconfig( + row, column, foreground="#666", selectforeground="#666" + ) + root.mainloop() + + +if __name__ == "__main__": + demo() diff --git a/nltk/draw/tree.py b/nltk/draw/tree.py new file mode 100644 index 0000000..a3037f9 --- /dev/null +++ b/nltk/draw/tree.py @@ -0,0 +1,1129 @@ +# Natural Language Toolkit: Graphical Representations for Trees +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Graphically display a Tree. +""" + +from tkinter import IntVar, Menu, Tk + +from nltk.draw.util import ( + BoxWidget, + CanvasFrame, + CanvasWidget, + OvalWidget, + ParenWidget, + TextWidget, +) +from nltk.tree import Tree +from nltk.util import in_idle + +##////////////////////////////////////////////////////// +## Tree Segment +##////////////////////////////////////////////////////// + + +class TreeSegmentWidget(CanvasWidget): + """ + A canvas widget that displays a single segment of a hierarchical + tree. Each ``TreeSegmentWidget`` connects a single "node widget" + to a sequence of zero or more "subtree widgets". By default, the + bottom of the node is connected to the top of each subtree by a + single line. However, if the ``roof`` attribute is set, then a + single triangular "roof" will connect the node to all of its + children. + + Attributes: + - ``roof``: What sort of connection to draw between the node and + its subtrees. If ``roof`` is true, draw a single triangular + "roof" over the subtrees. If ``roof`` is false, draw a line + between each subtree and the node. Default value is false. + - ``xspace``: The amount of horizontal space to leave between + subtrees when managing this widget. Default value is 10. + - ``yspace``: The amount of space to place between the node and + its children when managing this widget. Default value is 15. + - ``color``: The color of the lines connecting the node to its + subtrees; and of the outline of the triangular roof. Default + value is ``'#006060'``. + - ``fill``: The fill color for the triangular roof. Default + value is ``''`` (no fill). + - ``width``: The width of the lines connecting the node to its + subtrees; and of the outline of the triangular roof. Default + value is 1. + - ``orientation``: Determines whether the tree branches downwards + or rightwards. Possible values are ``'horizontal'`` and + ``'vertical'``. The default value is ``'vertical'`` (i.e., + branch downwards). + - ``draggable``: whether the widget can be dragged by the user. + """ + + def __init__(self, canvas, label, subtrees, **attribs): + """ + :type node: + :type subtrees: list(CanvasWidgetI) + """ + self._label = label + self._subtrees = subtrees + + # Attributes + self._horizontal = 0 + self._roof = 0 + self._xspace = 10 + self._yspace = 15 + self._ordered = False + + # Create canvas objects. + self._lines = [canvas.create_line(0, 0, 0, 0, fill="#006060") for c in subtrees] + self._polygon = canvas.create_polygon( + 0, 0, fill="", state="hidden", outline="#006060" + ) + + # Register child widgets (label + subtrees) + self._add_child_widget(label) + for subtree in subtrees: + self._add_child_widget(subtree) + + # Are we currently managing? + self._managing = False + + CanvasWidget.__init__(self, canvas, **attribs) + + def __setitem__(self, attr, value): + canvas = self.canvas() + if attr == "roof": + self._roof = value + if self._roof: + for l in self._lines: + canvas.itemconfig(l, state="hidden") + canvas.itemconfig(self._polygon, state="normal") + else: + for l in self._lines: + canvas.itemconfig(l, state="normal") + canvas.itemconfig(self._polygon, state="hidden") + elif attr == "orientation": + if value == "horizontal": + self._horizontal = 1 + elif value == "vertical": + self._horizontal = 0 + else: + raise ValueError("orientation must be horizontal or vertical") + elif attr == "color": + for l in self._lines: + canvas.itemconfig(l, fill=value) + canvas.itemconfig(self._polygon, outline=value) + elif isinstance(attr, tuple) and attr[0] == "color": + # Set the color of an individual line. + l = self._lines[int(attr[1])] + canvas.itemconfig(l, fill=value) + elif attr == "fill": + canvas.itemconfig(self._polygon, fill=value) + elif attr == "width": + canvas.itemconfig(self._polygon, {attr: value}) + for l in self._lines: + canvas.itemconfig(l, {attr: value}) + elif attr in ("xspace", "yspace"): + if attr == "xspace": + self._xspace = value + elif attr == "yspace": + self._yspace = value + self.update(self._label) + elif attr == "ordered": + self._ordered = value + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "roof": + return self._roof + elif attr == "width": + return self.canvas().itemcget(self._polygon, attr) + elif attr == "color": + return self.canvas().itemcget(self._polygon, "outline") + elif isinstance(attr, tuple) and attr[0] == "color": + l = self._lines[int(attr[1])] + return self.canvas().itemcget(l, "fill") + elif attr == "xspace": + return self._xspace + elif attr == "yspace": + return self._yspace + elif attr == "orientation": + if self._horizontal: + return "horizontal" + else: + return "vertical" + elif attr == "ordered": + return self._ordered + else: + return CanvasWidget.__getitem__(self, attr) + + def label(self): + return self._label + + def subtrees(self): + return self._subtrees[:] + + def set_label(self, label): + """ + Set the node label to ``label``. + """ + self._remove_child_widget(self._label) + self._add_child_widget(label) + self._label = label + self.update(self._label) + + def replace_child(self, oldchild, newchild): + """ + Replace the child ``oldchild`` with ``newchild``. + """ + index = self._subtrees.index(oldchild) + self._subtrees[index] = newchild + self._remove_child_widget(oldchild) + self._add_child_widget(newchild) + self.update(newchild) + + def remove_child(self, child): + index = self._subtrees.index(child) + del self._subtrees[index] + self._remove_child_widget(child) + self.canvas().delete(self._lines.pop()) + self.update(self._label) + + def insert_child(self, index, child): + canvas = self.canvas() + self._subtrees.insert(index, child) + self._add_child_widget(child) + self._lines.append(canvas.create_line(0, 0, 0, 0, fill="#006060")) + self.update(self._label) + + # but.. lines??? + + def _tags(self): + if self._roof: + return [self._polygon] + else: + return self._lines + + def _subtree_top(self, child): + if isinstance(child, TreeSegmentWidget): + bbox = child.label().bbox() + else: + bbox = child.bbox() + if self._horizontal: + return (bbox[0], (bbox[1] + bbox[3]) / 2.0) + else: + return ((bbox[0] + bbox[2]) / 2.0, bbox[1]) + + def _node_bottom(self): + bbox = self._label.bbox() + if self._horizontal: + return (bbox[2], (bbox[1] + bbox[3]) / 2.0) + else: + return ((bbox[0] + bbox[2]) / 2.0, bbox[3]) + + def _update(self, child): + if len(self._subtrees) == 0: + return + if self._label.bbox() is None: + return # [XX] ??? + + # Which lines need to be redrawn? + if child is self._label: + need_update = self._subtrees + else: + need_update = [child] + + if self._ordered and not self._managing: + need_update = self._maintain_order(child) + + # Update the polygon. + (nodex, nodey) = self._node_bottom() + (xmin, ymin, xmax, ymax) = self._subtrees[0].bbox() + for subtree in self._subtrees[1:]: + bbox = subtree.bbox() + xmin = min(xmin, bbox[0]) + ymin = min(ymin, bbox[1]) + xmax = max(xmax, bbox[2]) + ymax = max(ymax, bbox[3]) + + if self._horizontal: + self.canvas().coords( + self._polygon, nodex, nodey, xmin, ymin, xmin, ymax, nodex, nodey + ) + else: + self.canvas().coords( + self._polygon, nodex, nodey, xmin, ymin, xmax, ymin, nodex, nodey + ) + + # Redraw all lines that need it. + for subtree in need_update: + (nodex, nodey) = self._node_bottom() + line = self._lines[self._subtrees.index(subtree)] + (subtreex, subtreey) = self._subtree_top(subtree) + self.canvas().coords(line, nodex, nodey, subtreex, subtreey) + + def _maintain_order(self, child): + if self._horizontal: + return self._maintain_order_horizontal(child) + else: + return self._maintain_order_vertical(child) + + def _maintain_order_vertical(self, child): + (left, top, right, bot) = child.bbox() + + if child is self._label: + # Check all the leaves + for subtree in self._subtrees: + (x1, y1, x2, y2) = subtree.bbox() + if bot + self._yspace > y1: + subtree.move(0, bot + self._yspace - y1) + + return self._subtrees + else: + moved = [child] + index = self._subtrees.index(child) + + # Check leaves to our right. + x = right + self._xspace + for i in range(index + 1, len(self._subtrees)): + (x1, y1, x2, y2) = self._subtrees[i].bbox() + if x > x1: + self._subtrees[i].move(x - x1, 0) + x += x2 - x1 + self._xspace + moved.append(self._subtrees[i]) + + # Check leaves to our left. + x = left - self._xspace + for i in range(index - 1, -1, -1): + (x1, y1, x2, y2) = self._subtrees[i].bbox() + if x < x2: + self._subtrees[i].move(x - x2, 0) + x -= x2 - x1 + self._xspace + moved.append(self._subtrees[i]) + + # Check the node + (x1, y1, x2, y2) = self._label.bbox() + if y2 > top - self._yspace: + self._label.move(0, top - self._yspace - y2) + moved = self._subtrees + + # Return a list of the nodes we moved + return moved + + def _maintain_order_horizontal(self, child): + (left, top, right, bot) = child.bbox() + + if child is self._label: + # Check all the leaves + for subtree in self._subtrees: + (x1, y1, x2, y2) = subtree.bbox() + if right + self._xspace > x1: + subtree.move(right + self._xspace - x1) + + return self._subtrees + else: + moved = [child] + index = self._subtrees.index(child) + + # Check leaves below us. + y = bot + self._yspace + for i in range(index + 1, len(self._subtrees)): + (x1, y1, x2, y2) = self._subtrees[i].bbox() + if y > y1: + self._subtrees[i].move(0, y - y1) + y += y2 - y1 + self._yspace + moved.append(self._subtrees[i]) + + # Check leaves above us + y = top - self._yspace + for i in range(index - 1, -1, -1): + (x1, y1, x2, y2) = self._subtrees[i].bbox() + if y < y2: + self._subtrees[i].move(0, y - y2) + y -= y2 - y1 + self._yspace + moved.append(self._subtrees[i]) + + # Check the node + (x1, y1, x2, y2) = self._label.bbox() + if x2 > left - self._xspace: + self._label.move(left - self._xspace - x2, 0) + moved = self._subtrees + + # Return a list of the nodes we moved + return moved + + def _manage_horizontal(self): + (nodex, nodey) = self._node_bottom() + + # Put the subtrees in a line. + y = 20 + for subtree in self._subtrees: + subtree_bbox = subtree.bbox() + dx = nodex - subtree_bbox[0] + self._xspace + dy = y - subtree_bbox[1] + subtree.move(dx, dy) + y += subtree_bbox[3] - subtree_bbox[1] + self._yspace + + # Find the center of their tops. + center = 0.0 + for subtree in self._subtrees: + center += self._subtree_top(subtree)[1] + center /= len(self._subtrees) + + # Center the subtrees with the node. + for subtree in self._subtrees: + subtree.move(0, nodey - center) + + def _manage_vertical(self): + (nodex, nodey) = self._node_bottom() + + # Put the subtrees in a line. + x = 0 + for subtree in self._subtrees: + subtree_bbox = subtree.bbox() + dy = nodey - subtree_bbox[1] + self._yspace + dx = x - subtree_bbox[0] + subtree.move(dx, dy) + x += subtree_bbox[2] - subtree_bbox[0] + self._xspace + + # Find the center of their tops. + center = 0.0 + for subtree in self._subtrees: + center += self._subtree_top(subtree)[0] / len(self._subtrees) + + # Center the subtrees with the node. + for subtree in self._subtrees: + subtree.move(nodex - center, 0) + + def _manage(self): + self._managing = True + (nodex, nodey) = self._node_bottom() + if len(self._subtrees) == 0: + return + + if self._horizontal: + self._manage_horizontal() + else: + self._manage_vertical() + + # Update lines to subtrees. + for subtree in self._subtrees: + self._update(subtree) + + self._managing = False + + def __repr__(self): + return f"[TreeSeg {self._label}: {self._subtrees}]" + + +def _tree_to_treeseg( + canvas, + t, + make_node, + make_leaf, + tree_attribs, + node_attribs, + leaf_attribs, + loc_attribs, +): + if isinstance(t, Tree): + label = make_node(canvas, t.label(), **node_attribs) + subtrees = [ + _tree_to_treeseg( + canvas, + child, + make_node, + make_leaf, + tree_attribs, + node_attribs, + leaf_attribs, + loc_attribs, + ) + for child in t + ] + return TreeSegmentWidget(canvas, label, subtrees, **tree_attribs) + else: + return make_leaf(canvas, t, **leaf_attribs) + + +def tree_to_treesegment( + canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs +): + """ + Convert a Tree into a ``TreeSegmentWidget``. + + :param make_node: A ``CanvasWidget`` constructor or a function that + creates ``CanvasWidgets``. ``make_node`` is used to convert + the Tree's nodes into ``CanvasWidgets``. If no constructor + is specified, then ``TextWidget`` will be used. + :param make_leaf: A ``CanvasWidget`` constructor or a function that + creates ``CanvasWidgets``. ``make_leaf`` is used to convert + the Tree's leafs into ``CanvasWidgets``. If no constructor + is specified, then ``TextWidget`` will be used. + :param attribs: Attributes for the canvas widgets that make up the + returned ``TreeSegmentWidget``. Any attribute beginning with + ``'tree_'`` will be passed to all ``TreeSegmentWidgets`` (with + the ``'tree_'`` prefix removed. Any attribute beginning with + ``'node_'`` will be passed to all nodes. Any attribute + beginning with ``'leaf_'`` will be passed to all leaves. And + any attribute beginning with ``'loc_'`` will be passed to all + text locations (for Trees). + """ + # Process attribs. + tree_attribs = {} + node_attribs = {} + leaf_attribs = {} + loc_attribs = {} + + for key, value in list(attribs.items()): + if key[:5] == "tree_": + tree_attribs[key[5:]] = value + elif key[:5] == "node_": + node_attribs[key[5:]] = value + elif key[:5] == "leaf_": + leaf_attribs[key[5:]] = value + elif key[:4] == "loc_": + loc_attribs[key[4:]] = value + else: + raise ValueError("Bad attribute: %s" % key) + return _tree_to_treeseg( + canvas, + t, + make_node, + make_leaf, + tree_attribs, + node_attribs, + leaf_attribs, + loc_attribs, + ) + + +##////////////////////////////////////////////////////// +## Tree Widget +##////////////////////////////////////////////////////// + + +class TreeWidget(CanvasWidget): + """ + A canvas widget that displays a single Tree. + ``TreeWidget`` manages a group of ``TreeSegmentWidgets`` that are + used to display a Tree. + + Attributes: + + - ``node_attr``: Sets the attribute ``attr`` on all of the + node widgets for this ``TreeWidget``. + - ``node_attr``: Sets the attribute ``attr`` on all of the + leaf widgets for this ``TreeWidget``. + - ``loc_attr``: Sets the attribute ``attr`` on all of the + location widgets for this ``TreeWidget`` (if it was built from + a Tree). Note that a location widget is a ``TextWidget``. + + - ``xspace``: The amount of horizontal space to leave between + subtrees when managing this widget. Default value is 10. + - ``yspace``: The amount of space to place between the node and + its children when managing this widget. Default value is 15. + + - ``line_color``: The color of the lines connecting each expanded + node to its subtrees. + - ``roof_color``: The color of the outline of the triangular roof + for collapsed trees. + - ``roof_fill``: The fill color for the triangular roof for + collapsed trees. + - ``width`` + + - ``orientation``: Determines whether the tree branches downwards + or rightwards. Possible values are ``'horizontal'`` and + ``'vertical'``. The default value is ``'vertical'`` (i.e., + branch downwards). + + - ``shapeable``: whether the subtrees can be independently + dragged by the user. THIS property simply sets the + ``DRAGGABLE`` property on all of the ``TreeWidget``'s tree + segments. + - ``draggable``: whether the widget can be dragged by the user. + """ + + def __init__( + self, canvas, t, make_node=TextWidget, make_leaf=TextWidget, **attribs + ): + # Node & leaf canvas widget constructors + self._make_node = make_node + self._make_leaf = make_leaf + self._tree = t + + # Attributes. + self._nodeattribs = {} + self._leafattribs = {} + self._locattribs = {"color": "#008000"} + self._line_color = "#008080" + self._line_width = 1 + self._roof_color = "#008080" + self._roof_fill = "#c0c0c0" + self._shapeable = False + self._xspace = 10 + self._yspace = 10 + self._orientation = "vertical" + self._ordered = False + + # Build trees. + self._keys = {} # treeseg -> key + self._expanded_trees = {} + self._collapsed_trees = {} + self._nodes = [] + self._leaves = [] + # self._locs = [] + self._make_collapsed_trees(canvas, t, ()) + self._treeseg = self._make_expanded_tree(canvas, t, ()) + self._add_child_widget(self._treeseg) + + CanvasWidget.__init__(self, canvas, **attribs) + + def expanded_tree(self, *path_to_tree): + """ + Return the ``TreeSegmentWidget`` for the specified subtree. + + :param path_to_tree: A list of indices i1, i2, ..., in, where + the desired widget is the widget corresponding to + ``tree.children()[i1].children()[i2]....children()[in]``. + For the root, the path is ``()``. + """ + return self._expanded_trees[path_to_tree] + + def collapsed_tree(self, *path_to_tree): + """ + Return the ``TreeSegmentWidget`` for the specified subtree. + + :param path_to_tree: A list of indices i1, i2, ..., in, where + the desired widget is the widget corresponding to + ``tree.children()[i1].children()[i2]....children()[in]``. + For the root, the path is ``()``. + """ + return self._collapsed_trees[path_to_tree] + + def bind_click_trees(self, callback, button=1): + """ + Add a binding to all tree segments. + """ + for tseg in list(self._expanded_trees.values()): + tseg.bind_click(callback, button) + for tseg in list(self._collapsed_trees.values()): + tseg.bind_click(callback, button) + + def bind_drag_trees(self, callback, button=1): + """ + Add a binding to all tree segments. + """ + for tseg in list(self._expanded_trees.values()): + tseg.bind_drag(callback, button) + for tseg in list(self._collapsed_trees.values()): + tseg.bind_drag(callback, button) + + def bind_click_leaves(self, callback, button=1): + """ + Add a binding to all leaves. + """ + for leaf in self._leaves: + leaf.bind_click(callback, button) + for leaf in self._leaves: + leaf.bind_click(callback, button) + + def bind_drag_leaves(self, callback, button=1): + """ + Add a binding to all leaves. + """ + for leaf in self._leaves: + leaf.bind_drag(callback, button) + for leaf in self._leaves: + leaf.bind_drag(callback, button) + + def bind_click_nodes(self, callback, button=1): + """ + Add a binding to all nodes. + """ + for node in self._nodes: + node.bind_click(callback, button) + for node in self._nodes: + node.bind_click(callback, button) + + def bind_drag_nodes(self, callback, button=1): + """ + Add a binding to all nodes. + """ + for node in self._nodes: + node.bind_drag(callback, button) + for node in self._nodes: + node.bind_drag(callback, button) + + def _make_collapsed_trees(self, canvas, t, key): + if not isinstance(t, Tree): + return + make_node = self._make_node + make_leaf = self._make_leaf + + node = make_node(canvas, t.label(), **self._nodeattribs) + self._nodes.append(node) + leaves = [make_leaf(canvas, l, **self._leafattribs) for l in t.leaves()] + self._leaves += leaves + treeseg = TreeSegmentWidget( + canvas, + node, + leaves, + roof=1, + color=self._roof_color, + fill=self._roof_fill, + width=self._line_width, + ) + + self._collapsed_trees[key] = treeseg + self._keys[treeseg] = key + # self._add_child_widget(treeseg) + treeseg.hide() + + # Build trees for children. + for i in range(len(t)): + child = t[i] + self._make_collapsed_trees(canvas, child, key + (i,)) + + def _make_expanded_tree(self, canvas, t, key): + make_node = self._make_node + make_leaf = self._make_leaf + + if isinstance(t, Tree): + node = make_node(canvas, t.label(), **self._nodeattribs) + self._nodes.append(node) + children = t + subtrees = [ + self._make_expanded_tree(canvas, children[i], key + (i,)) + for i in range(len(children)) + ] + treeseg = TreeSegmentWidget( + canvas, node, subtrees, color=self._line_color, width=self._line_width + ) + self._expanded_trees[key] = treeseg + self._keys[treeseg] = key + return treeseg + else: + leaf = make_leaf(canvas, t, **self._leafattribs) + self._leaves.append(leaf) + return leaf + + def __setitem__(self, attr, value): + if attr[:5] == "node_": + for node in self._nodes: + node[attr[5:]] = value + elif attr[:5] == "leaf_": + for leaf in self._leaves: + leaf[attr[5:]] = value + elif attr == "line_color": + self._line_color = value + for tseg in list(self._expanded_trees.values()): + tseg["color"] = value + elif attr == "line_width": + self._line_width = value + for tseg in list(self._expanded_trees.values()): + tseg["width"] = value + for tseg in list(self._collapsed_trees.values()): + tseg["width"] = value + elif attr == "roof_color": + self._roof_color = value + for tseg in list(self._collapsed_trees.values()): + tseg["color"] = value + elif attr == "roof_fill": + self._roof_fill = value + for tseg in list(self._collapsed_trees.values()): + tseg["fill"] = value + elif attr == "shapeable": + self._shapeable = value + for tseg in list(self._expanded_trees.values()): + tseg["draggable"] = value + for tseg in list(self._collapsed_trees.values()): + tseg["draggable"] = value + for leaf in self._leaves: + leaf["draggable"] = value + elif attr == "xspace": + self._xspace = value + for tseg in list(self._expanded_trees.values()): + tseg["xspace"] = value + for tseg in list(self._collapsed_trees.values()): + tseg["xspace"] = value + self.manage() + elif attr == "yspace": + self._yspace = value + for tseg in list(self._expanded_trees.values()): + tseg["yspace"] = value + for tseg in list(self._collapsed_trees.values()): + tseg["yspace"] = value + self.manage() + elif attr == "orientation": + self._orientation = value + for tseg in list(self._expanded_trees.values()): + tseg["orientation"] = value + for tseg in list(self._collapsed_trees.values()): + tseg["orientation"] = value + self.manage() + elif attr == "ordered": + self._ordered = value + for tseg in list(self._expanded_trees.values()): + tseg["ordered"] = value + for tseg in list(self._collapsed_trees.values()): + tseg["ordered"] = value + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr[:5] == "node_": + return self._nodeattribs.get(attr[5:], None) + elif attr[:5] == "leaf_": + return self._leafattribs.get(attr[5:], None) + elif attr[:4] == "loc_": + return self._locattribs.get(attr[4:], None) + elif attr == "line_color": + return self._line_color + elif attr == "line_width": + return self._line_width + elif attr == "roof_color": + return self._roof_color + elif attr == "roof_fill": + return self._roof_fill + elif attr == "shapeable": + return self._shapeable + elif attr == "xspace": + return self._xspace + elif attr == "yspace": + return self._yspace + elif attr == "orientation": + return self._orientation + else: + return CanvasWidget.__getitem__(self, attr) + + def _tags(self): + return [] + + def _manage(self): + segs = list(self._expanded_trees.values()) + list( + self._collapsed_trees.values() + ) + for tseg in segs: + if tseg.hidden(): + tseg.show() + tseg.manage() + tseg.hide() + + def toggle_collapsed(self, treeseg): + """ + Collapse/expand a tree. + """ + old_treeseg = treeseg + if old_treeseg["roof"]: + new_treeseg = self._expanded_trees[self._keys[old_treeseg]] + else: + new_treeseg = self._collapsed_trees[self._keys[old_treeseg]] + + # Replace the old tree with the new tree. + if old_treeseg.parent() is self: + self._remove_child_widget(old_treeseg) + self._add_child_widget(new_treeseg) + self._treeseg = new_treeseg + else: + old_treeseg.parent().replace_child(old_treeseg, new_treeseg) + + # Move the new tree to where the old tree was. Show it first, + # so we can find its bounding box. + new_treeseg.show() + (newx, newy) = new_treeseg.label().bbox()[:2] + (oldx, oldy) = old_treeseg.label().bbox()[:2] + new_treeseg.move(oldx - newx, oldy - newy) + + # Hide the old tree + old_treeseg.hide() + + # We could do parent.manage() here instead, if we wanted. + new_treeseg.parent().update(new_treeseg) + + +##////////////////////////////////////////////////////// +## draw_trees +##////////////////////////////////////////////////////// + + +class TreeView: + def __init__(self, *trees): + from math import ceil, sqrt + + self._trees = trees + + self._top = Tk() + self._top.title("NLTK") + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + + cf = self._cframe = CanvasFrame(self._top) + self._top.bind("", self._cframe.print_to_file) + + # Size is variable. + self._size = IntVar(self._top) + self._size.set(12) + bold = ("helvetica", -self._size.get(), "bold") + helv = ("helvetica", -self._size.get()) + + # Lay the trees out in a square. + self._width = int(ceil(sqrt(len(trees)))) + self._widgets = [] + for i in range(len(trees)): + widget = TreeWidget( + cf.canvas(), + trees[i], + node_font=bold, + leaf_color="#008040", + node_color="#004080", + roof_color="#004040", + roof_fill="white", + line_color="#004040", + draggable=1, + leaf_font=helv, + ) + widget.bind_click_trees(widget.toggle_collapsed) + self._widgets.append(widget) + cf.add_widget(widget, 0, 0) + + self._layout() + self._cframe.pack(expand=1, fill="both") + self._init_menubar() + + def _layout(self): + i = x = y = ymax = 0 + width = self._width + for i in range(len(self._widgets)): + widget = self._widgets[i] + (oldx, oldy) = widget.bbox()[:2] + if i % width == 0: + y = ymax + x = 0 + widget.move(x - oldx, y - oldy) + x = widget.bbox()[2] + 10 + ymax = max(ymax, widget.bbox()[3] + 10) + + def _init_menubar(self): + menubar = Menu(self._top) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Print to Postscript", + underline=0, + command=self._cframe.print_to_file, + accelerator="Ctrl-p", + ) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + zoommenu = Menu(menubar, tearoff=0) + zoommenu.add_radiobutton( + label="Tiny", + variable=self._size, + underline=0, + value=10, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Small", + variable=self._size, + underline=0, + value=12, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Medium", + variable=self._size, + underline=0, + value=14, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Large", + variable=self._size, + underline=0, + value=28, + command=self.resize, + ) + zoommenu.add_radiobutton( + label="Huge", + variable=self._size, + underline=0, + value=50, + command=self.resize, + ) + menubar.add_cascade(label="Zoom", underline=0, menu=zoommenu) + + self._top.config(menu=menubar) + + def resize(self, *e): + bold = ("helvetica", -self._size.get(), "bold") + helv = ("helvetica", -self._size.get()) + xspace = self._size.get() + yspace = self._size.get() + for widget in self._widgets: + widget["node_font"] = bold + widget["leaf_font"] = helv + widget["xspace"] = xspace + widget["yspace"] = yspace + if self._size.get() < 20: + widget["line_width"] = 1 + elif self._size.get() < 30: + widget["line_width"] = 2 + else: + widget["line_width"] = 3 + self._layout() + + def destroy(self, *e): + if self._top is None: + return + self._top.destroy() + self._top = None + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this demo is created from a non-interactive program (e.g. + from a secript); otherwise, the demo will close as soon as + the script completes. + """ + if in_idle(): + return + self._top.mainloop(*args, **kwargs) + + +def draw_trees(*trees): + """ + Open a new window containing a graphical diagram of the given + trees. + + :rtype: None + """ + TreeView(*trees).mainloop() + return + + +##////////////////////////////////////////////////////// +## Demo Code +##////////////////////////////////////////////////////// + + +def demo(): + import random + + def fill(cw): + cw["fill"] = "#%06d" % random.randint(0, 999999) + + cf = CanvasFrame(width=550, height=450, closeenough=2) + + t = Tree.fromstring( + """ + (S (NP the very big cat) + (VP (Adv sorta) (V saw) (NP (Det the) (N dog))))""" + ) + + tc = TreeWidget( + cf.canvas(), + t, + draggable=1, + node_font=("helvetica", -14, "bold"), + leaf_font=("helvetica", -12, "italic"), + roof_fill="white", + roof_color="black", + leaf_color="green4", + node_color="blue2", + ) + cf.add_widget(tc, 10, 10) + + def boxit(canvas, text): + big = ("helvetica", -16, "bold") + return BoxWidget(canvas, TextWidget(canvas, text, font=big), fill="green") + + def ovalit(canvas, text): + return OvalWidget(canvas, TextWidget(canvas, text), fill="cyan") + + treetok = Tree.fromstring("(S (NP this tree) (VP (V is) (AdjP shapeable)))") + tc2 = TreeWidget(cf.canvas(), treetok, boxit, ovalit, shapeable=1) + + def color(node): + node["color"] = "#%04d00" % random.randint(0, 9999) + + def color2(treeseg): + treeseg.label()["fill"] = "#%06d" % random.randint(0, 9999) + treeseg.label().child()["color"] = "white" + + tc.bind_click_trees(tc.toggle_collapsed) + tc2.bind_click_trees(tc2.toggle_collapsed) + tc.bind_click_nodes(color, 3) + tc2.expanded_tree(1).bind_click(color2, 3) + tc2.expanded_tree().bind_click(color2, 3) + + paren = ParenWidget(cf.canvas(), tc2) + cf.add_widget(paren, tc.bbox()[2] + 10, 10) + + tree3 = Tree.fromstring( + """ + (S (NP this tree) (AUX was) + (VP (V built) (PP (P with) (NP (N tree_to_treesegment)))))""" + ) + tc3 = tree_to_treesegment( + cf.canvas(), tree3, tree_color="green4", tree_xspace=2, tree_width=2 + ) + tc3["draggable"] = 1 + cf.add_widget(tc3, 10, tc.bbox()[3] + 10) + + def orientswitch(treewidget): + if treewidget["orientation"] == "horizontal": + treewidget.expanded_tree(1, 1).subtrees()[0].set_text("vertical") + treewidget.collapsed_tree(1, 1).subtrees()[0].set_text("vertical") + treewidget.collapsed_tree(1).subtrees()[1].set_text("vertical") + treewidget.collapsed_tree().subtrees()[3].set_text("vertical") + treewidget["orientation"] = "vertical" + else: + treewidget.expanded_tree(1, 1).subtrees()[0].set_text("horizontal") + treewidget.collapsed_tree(1, 1).subtrees()[0].set_text("horizontal") + treewidget.collapsed_tree(1).subtrees()[1].set_text("horizontal") + treewidget.collapsed_tree().subtrees()[3].set_text("horizontal") + treewidget["orientation"] = "horizontal" + + text = """ +Try clicking, right clicking, and dragging +different elements of each of the trees. +The top-left tree is a TreeWidget built from +a Tree. The top-right is a TreeWidget built +from a Tree, using non-default widget +constructors for the nodes & leaves (BoxWidget +and OvalWidget). The bottom-left tree is +built from tree_to_treesegment.""" + twidget = TextWidget(cf.canvas(), text.strip()) + textbox = BoxWidget(cf.canvas(), twidget, fill="white", draggable=1) + cf.add_widget(textbox, tc3.bbox()[2] + 10, tc2.bbox()[3] + 10) + + tree4 = Tree.fromstring("(S (NP this tree) (VP (V is) (Adj horizontal)))") + tc4 = TreeWidget( + cf.canvas(), + tree4, + draggable=1, + line_color="brown2", + roof_color="brown2", + node_font=("helvetica", -12, "bold"), + node_color="brown4", + orientation="horizontal", + ) + tc4.manage() + cf.add_widget(tc4, tc3.bbox()[2] + 10, textbox.bbox()[3] + 10) + tc4.bind_click(orientswitch) + tc4.bind_click_trees(tc4.toggle_collapsed, 3) + + # Run mainloop + cf.mainloop() + + +if __name__ == "__main__": + demo() diff --git a/nltk/draw/util.py b/nltk/draw/util.py new file mode 100644 index 0000000..db3b733 --- /dev/null +++ b/nltk/draw/util.py @@ -0,0 +1,2575 @@ +# Natural Language Toolkit: Drawing utilities +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Tools for graphically displaying and interacting with the objects and +processing classes defined by the Toolkit. These tools are primarily +intended to help students visualize the objects that they create. + +The graphical tools are typically built using "canvas widgets", each +of which encapsulates the graphical elements and bindings used to +display a complex object on a Tkinter ``Canvas``. For example, NLTK +defines canvas widgets for displaying trees and directed graphs, as +well as a number of simpler widgets. These canvas widgets make it +easier to build new graphical tools and demos. See the class +documentation for ``CanvasWidget`` for more information. + +The ``nltk.draw`` module defines the abstract ``CanvasWidget`` base +class, and a number of simple canvas widgets. The remaining canvas +widgets are defined by submodules, such as ``nltk.draw.tree``. + +The ``nltk.draw`` module also defines ``CanvasFrame``, which +encapsulates a ``Canvas`` and its scrollbars. It uses a +``ScrollWatcherWidget`` to ensure that all canvas widgets contained on +its canvas are within the scroll region. + +Acknowledgements: Many of the ideas behind the canvas widget system +are derived from ``CLIG``, a Tk-based grapher for linguistic data +structures. For more information, see the CLIG +homepage (http://www.ags.uni-sb.de/~konrad/clig.html). + +""" +from abc import ABCMeta, abstractmethod +from tkinter import ( + RAISED, + Button, + Canvas, + Entry, + Frame, + Label, + Menu, + Menubutton, + Scrollbar, + StringVar, + Text, + Tk, + Toplevel, + Widget, +) +from tkinter.filedialog import asksaveasfilename + +from nltk.util import in_idle + +##////////////////////////////////////////////////////// +## CanvasWidget +##////////////////////////////////////////////////////// + + +class CanvasWidget(metaclass=ABCMeta): + """ + A collection of graphical elements and bindings used to display a + complex object on a Tkinter ``Canvas``. A canvas widget is + responsible for managing the ``Canvas`` tags and callback bindings + necessary to display and interact with the object. Canvas widgets + are often organized into hierarchies, where parent canvas widgets + control aspects of their child widgets. + + Each canvas widget is bound to a single ``Canvas``. This ``Canvas`` + is specified as the first argument to the ``CanvasWidget``'s + constructor. + + Attributes. Each canvas widget can support a variety of + "attributes", which control how the canvas widget is displayed. + Some typical examples attributes are ``color``, ``font``, and + ``radius``. Each attribute has a default value. This default + value can be overridden in the constructor, using keyword + arguments of the form ``attribute=value``: + + >>> from nltk.draw.util import TextWidget + >>> cn = TextWidget(Canvas(), 'test', color='red') # doctest: +SKIP + + Attribute values can also be changed after a canvas widget has + been constructed, using the ``__setitem__`` operator: + + >>> cn['font'] = 'times' # doctest: +SKIP + + The current value of an attribute value can be queried using the + ``__getitem__`` operator: + + >>> cn['color'] # doctest: +SKIP + 'red' + + For a list of the attributes supported by a type of canvas widget, + see its class documentation. + + Interaction. The attribute ``'draggable'`` controls whether the + user can drag a canvas widget around the canvas. By default, + canvas widgets are not draggable. + + ``CanvasWidget`` provides callback support for two types of user + interaction: clicking and dragging. The method ``bind_click`` + registers a callback function that is called whenever the canvas + widget is clicked. The method ``bind_drag`` registers a callback + function that is called after the canvas widget is dragged. If + the user clicks or drags a canvas widget with no registered + callback function, then the interaction event will propagate to + its parent. For each canvas widget, only one callback function + may be registered for an interaction event. Callback functions + can be deregistered with the ``unbind_click`` and ``unbind_drag`` + methods. + + Subclassing. ``CanvasWidget`` is an abstract class. Subclasses + are required to implement the following methods: + + - ``__init__``: Builds a new canvas widget. It must perform the + following three tasks (in order): + + - Create any new graphical elements. + - Call ``_add_child_widget`` on each child widget. + - Call the ``CanvasWidget`` constructor. + - ``_tags``: Returns a list of the canvas tags for all graphical + elements managed by this canvas widget, not including + graphical elements managed by its child widgets. + - ``_manage``: Arranges the child widgets of this canvas widget. + This is typically only called when the canvas widget is + created. + - ``_update``: Update this canvas widget in response to a + change in a single child. + + For a ``CanvasWidget`` with no child widgets, the default + definitions for ``_manage`` and ``_update`` may be used. + + If a subclass defines any attributes, then it should implement + ``__getitem__`` and ``__setitem__``. If either of these methods is + called with an unknown attribute, then they should propagate the + request to ``CanvasWidget``. + + Most subclasses implement a number of additional methods that + modify the ``CanvasWidget`` in some way. These methods must call + ``parent.update(self)`` after making any changes to the canvas + widget's graphical elements. The canvas widget must also call + ``parent.update(self)`` after changing any attribute value that + affects the shape or position of the canvas widget's graphical + elements. + + :type __canvas: Tkinter.Canvas + :ivar __canvas: This ``CanvasWidget``'s canvas. + + :type __parent: CanvasWidget or None + :ivar __parent: This ``CanvasWidget``'s hierarchical parent widget. + :type __children: list(CanvasWidget) + :ivar __children: This ``CanvasWidget``'s hierarchical child widgets. + + :type __updating: bool + :ivar __updating: Is this canvas widget currently performing an + update? If it is, then it will ignore any new update requests + from child widgets. + + :type __draggable: bool + :ivar __draggable: Is this canvas widget draggable? + :type __press: event + :ivar __press: The ButtonPress event that we're currently handling. + :type __drag_x: int + :ivar __drag_x: Where it's been moved to (to find dx) + :type __drag_y: int + :ivar __drag_y: Where it's been moved to (to find dy) + :type __callbacks: dictionary + :ivar __callbacks: Registered callbacks. Currently, four keys are + used: ``1``, ``2``, ``3``, and ``'drag'``. The values are + callback functions. Each callback function takes a single + argument, which is the ``CanvasWidget`` that triggered the + callback. + """ + + def __init__(self, canvas, parent=None, **attribs): + """ + Create a new canvas widget. This constructor should only be + called by subclass constructors; and it should be called only + "after" the subclass has constructed all graphical canvas + objects and registered all child widgets. + + :param canvas: This canvas widget's canvas. + :type canvas: Tkinter.Canvas + :param parent: This canvas widget's hierarchical parent. + :type parent: CanvasWidget + :param attribs: The new canvas widget's attributes. + """ + if self.__class__ == CanvasWidget: + raise TypeError("CanvasWidget is an abstract base class") + + if not isinstance(canvas, Canvas): + raise TypeError("Expected a canvas!") + + self.__canvas = canvas + self.__parent = parent + + # If the subclass constructor called _add_child_widget, then + # self.__children will already exist. + if not hasattr(self, "_CanvasWidget__children"): + self.__children = [] + + # Is this widget hidden? + self.__hidden = 0 + + # Update control (prevents infinite loops) + self.__updating = 0 + + # Button-press and drag callback handling. + self.__press = None + self.__drag_x = self.__drag_y = 0 + self.__callbacks = {} + self.__draggable = 0 + + # Set up attributes. + for attr, value in list(attribs.items()): + self[attr] = value + + # Manage this canvas widget + self._manage() + + # Register any new bindings + for tag in self._tags(): + self.__canvas.tag_bind(tag, "", self.__press_cb) + self.__canvas.tag_bind(tag, "", self.__press_cb) + self.__canvas.tag_bind(tag, "", self.__press_cb) + + ##////////////////////////////////////////////////////// + ## Inherited methods. + ##////////////////////////////////////////////////////// + + def bbox(self): + """ + :return: A bounding box for this ``CanvasWidget``. The bounding + box is a tuple of four coordinates, *(xmin, ymin, xmax, ymax)*, + for a rectangle which encloses all of the canvas + widget's graphical elements. Bounding box coordinates are + specified with respect to the coordinate space of the ``Canvas``. + :rtype: tuple(int, int, int, int) + """ + if self.__hidden: + return (0, 0, 0, 0) + if len(self.tags()) == 0: + raise ValueError("No tags") + return self.__canvas.bbox(*self.tags()) + + def width(self): + """ + :return: The width of this canvas widget's bounding box, in + its ``Canvas``'s coordinate space. + :rtype: int + """ + if len(self.tags()) == 0: + raise ValueError("No tags") + bbox = self.__canvas.bbox(*self.tags()) + return bbox[2] - bbox[0] + + def height(self): + """ + :return: The height of this canvas widget's bounding box, in + its ``Canvas``'s coordinate space. + :rtype: int + """ + if len(self.tags()) == 0: + raise ValueError("No tags") + bbox = self.__canvas.bbox(*self.tags()) + return bbox[3] - bbox[1] + + def parent(self): + """ + :return: The hierarchical parent of this canvas widget. + ``self`` is considered a subpart of its parent for + purposes of user interaction. + :rtype: CanvasWidget or None + """ + return self.__parent + + def child_widgets(self): + """ + :return: A list of the hierarchical children of this canvas + widget. These children are considered part of ``self`` + for purposes of user interaction. + :rtype: list of CanvasWidget + """ + return self.__children + + def canvas(self): + """ + :return: The canvas that this canvas widget is bound to. + :rtype: Tkinter.Canvas + """ + return self.__canvas + + def move(self, dx, dy): + """ + Move this canvas widget by a given distance. In particular, + shift the canvas widget right by ``dx`` pixels, and down by + ``dy`` pixels. Both ``dx`` and ``dy`` may be negative, resulting + in leftward or upward movement. + + :type dx: int + :param dx: The number of pixels to move this canvas widget + rightwards. + :type dy: int + :param dy: The number of pixels to move this canvas widget + downwards. + :rtype: None + """ + if dx == dy == 0: + return + for tag in self.tags(): + self.__canvas.move(tag, dx, dy) + if self.__parent: + self.__parent.update(self) + + def moveto(self, x, y, anchor="NW"): + """ + Move this canvas widget to the given location. In particular, + shift the canvas widget such that the corner or side of the + bounding box specified by ``anchor`` is at location (``x``, + ``y``). + + :param x,y: The location that the canvas widget should be moved + to. + :param anchor: The corner or side of the canvas widget that + should be moved to the specified location. ``'N'`` + specifies the top center; ``'NE'`` specifies the top right + corner; etc. + """ + x1, y1, x2, y2 = self.bbox() + if anchor == "NW": + self.move(x - x1, y - y1) + if anchor == "N": + self.move(x - x1 / 2 - x2 / 2, y - y1) + if anchor == "NE": + self.move(x - x2, y - y1) + if anchor == "E": + self.move(x - x2, y - y1 / 2 - y2 / 2) + if anchor == "SE": + self.move(x - x2, y - y2) + if anchor == "S": + self.move(x - x1 / 2 - x2 / 2, y - y2) + if anchor == "SW": + self.move(x - x1, y - y2) + if anchor == "W": + self.move(x - x1, y - y1 / 2 - y2 / 2) + + def destroy(self): + """ + Remove this ``CanvasWidget`` from its ``Canvas``. After a + ``CanvasWidget`` has been destroyed, it should not be accessed. + + Note that you only need to destroy a top-level + ``CanvasWidget``; its child widgets will be destroyed + automatically. If you destroy a non-top-level + ``CanvasWidget``, then the entire top-level widget will be + destroyed. + + :raise ValueError: if this ``CanvasWidget`` has a parent. + :rtype: None + """ + if self.__parent is not None: + self.__parent.destroy() + return + + for tag in self.tags(): + self.__canvas.tag_unbind(tag, "") + self.__canvas.tag_unbind(tag, "") + self.__canvas.tag_unbind(tag, "") + self.__canvas.delete(*self.tags()) + self.__canvas = None + + def update(self, child): + """ + Update the graphical display of this canvas widget, and all of + its ancestors, in response to a change in one of this canvas + widget's children. + + :param child: The child widget that changed. + :type child: CanvasWidget + """ + if self.__hidden or child.__hidden: + return + # If we're already updating, then do nothing. This prevents + # infinite loops when _update modifies its children. + if self.__updating: + return + self.__updating = 1 + + # Update this CanvasWidget. + self._update(child) + + # Propagate update request to the parent. + if self.__parent: + self.__parent.update(self) + + # We're done updating. + self.__updating = 0 + + def manage(self): + """ + Arrange this canvas widget and all of its descendants. + + :rtype: None + """ + if self.__hidden: + return + for child in self.__children: + child.manage() + self._manage() + + def tags(self): + """ + :return: a list of the canvas tags for all graphical + elements managed by this canvas widget, including + graphical elements managed by its child widgets. + :rtype: list of int + """ + if self.__canvas is None: + raise ValueError("Attempt to access a destroyed canvas widget") + tags = [] + tags += self._tags() + for child in self.__children: + tags += child.tags() + return tags + + def __setitem__(self, attr, value): + """ + Set the value of the attribute ``attr`` to ``value``. See the + class documentation for a list of attributes supported by this + canvas widget. + + :rtype: None + """ + if attr == "draggable": + self.__draggable = value + else: + raise ValueError("Unknown attribute %r" % attr) + + def __getitem__(self, attr): + """ + :return: the value of the attribute ``attr``. See the class + documentation for a list of attributes supported by this + canvas widget. + :rtype: (any) + """ + if attr == "draggable": + return self.__draggable + else: + raise ValueError("Unknown attribute %r" % attr) + + def __repr__(self): + """ + :return: a string representation of this canvas widget. + :rtype: str + """ + return "<%s>" % self.__class__.__name__ + + def hide(self): + """ + Temporarily hide this canvas widget. + + :rtype: None + """ + self.__hidden = 1 + for tag in self.tags(): + self.__canvas.itemconfig(tag, state="hidden") + + def show(self): + """ + Show a hidden canvas widget. + + :rtype: None + """ + self.__hidden = 0 + for tag in self.tags(): + self.__canvas.itemconfig(tag, state="normal") + + def hidden(self): + """ + :return: True if this canvas widget is hidden. + :rtype: bool + """ + return self.__hidden + + ##////////////////////////////////////////////////////// + ## Callback interface + ##////////////////////////////////////////////////////// + + def bind_click(self, callback, button=1): + """ + Register a new callback that will be called whenever this + ``CanvasWidget`` is clicked on. + + :type callback: function + :param callback: The callback function that will be called + whenever this ``CanvasWidget`` is clicked. This function + will be called with this ``CanvasWidget`` as its argument. + :type button: int + :param button: Which button the user should use to click on + this ``CanvasWidget``. Typically, this should be 1 (left + button), 3 (right button), or 2 (middle button). + """ + self.__callbacks[button] = callback + + def bind_drag(self, callback): + """ + Register a new callback that will be called after this + ``CanvasWidget`` is dragged. This implicitly makes this + ``CanvasWidget`` draggable. + + :type callback: function + :param callback: The callback function that will be called + whenever this ``CanvasWidget`` is clicked. This function + will be called with this ``CanvasWidget`` as its argument. + """ + self.__draggable = 1 + self.__callbacks["drag"] = callback + + def unbind_click(self, button=1): + """ + Remove a callback that was registered with ``bind_click``. + + :type button: int + :param button: Which button the user should use to click on + this ``CanvasWidget``. Typically, this should be 1 (left + button), 3 (right button), or 2 (middle button). + """ + try: + del self.__callbacks[button] + except Exception: + pass + + def unbind_drag(self): + """ + Remove a callback that was registered with ``bind_drag``. + """ + try: + del self.__callbacks["drag"] + except Exception: + pass + + ##////////////////////////////////////////////////////// + ## Callback internals + ##////////////////////////////////////////////////////// + + def __press_cb(self, event): + """ + Handle a button-press event: + - record the button press event in ``self.__press`` + - register a button-release callback. + - if this CanvasWidget or any of its ancestors are + draggable, then register the appropriate motion callback. + """ + # If we're already waiting for a button release, then ignore + # this new button press. + if ( + self.__canvas.bind("") + or self.__canvas.bind("") + or self.__canvas.bind("") + ): + return + + # Unbind motion (just in case; this shouldn't be necessary) + self.__canvas.unbind("") + + # Record the button press event. + self.__press = event + + # If any ancestor is draggable, set up a motion callback. + # (Only if they pressed button number 1) + if event.num == 1: + widget = self + while widget is not None: + if widget["draggable"]: + widget.__start_drag(event) + break + widget = widget.parent() + + # Set up the button release callback. + self.__canvas.bind("" % event.num, self.__release_cb) + + def __start_drag(self, event): + """ + Begin dragging this object: + - register a motion callback + - record the drag coordinates + """ + self.__canvas.bind("", self.__motion_cb) + self.__drag_x = event.x + self.__drag_y = event.y + + def __motion_cb(self, event): + """ + Handle a motion event: + - move this object to the new location + - record the new drag coordinates + """ + self.move(event.x - self.__drag_x, event.y - self.__drag_y) + self.__drag_x = event.x + self.__drag_y = event.y + + def __release_cb(self, event): + """ + Handle a release callback: + - unregister motion & button release callbacks. + - decide whether they clicked, dragged, or cancelled + - call the appropriate handler. + """ + # Unbind the button release & motion callbacks. + self.__canvas.unbind("" % event.num) + self.__canvas.unbind("") + + # Is it a click or a drag? + if ( + event.time - self.__press.time < 100 + and abs(event.x - self.__press.x) + abs(event.y - self.__press.y) < 5 + ): + # Move it back, if we were dragging. + if self.__draggable and event.num == 1: + self.move( + self.__press.x - self.__drag_x, self.__press.y - self.__drag_y + ) + self.__click(event.num) + elif event.num == 1: + self.__drag() + + self.__press = None + + def __drag(self): + """ + If this ``CanvasWidget`` has a drag callback, then call it; + otherwise, find the closest ancestor with a drag callback, and + call it. If no ancestors have a drag callback, do nothing. + """ + if self.__draggable: + if "drag" in self.__callbacks: + cb = self.__callbacks["drag"] + try: + cb(self) + except Exception: + print("Error in drag callback for %r" % self) + elif self.__parent is not None: + self.__parent.__drag() + + def __click(self, button): + """ + If this ``CanvasWidget`` has a drag callback, then call it; + otherwise, find the closest ancestor with a click callback, and + call it. If no ancestors have a click callback, do nothing. + """ + if button in self.__callbacks: + cb = self.__callbacks[button] + # try: + cb(self) + # except Exception: + # print('Error in click callback for %r' % self) + # raise + elif self.__parent is not None: + self.__parent.__click(button) + + ##////////////////////////////////////////////////////// + ## Child/parent Handling + ##////////////////////////////////////////////////////// + + def _add_child_widget(self, child): + """ + Register a hierarchical child widget. The child will be + considered part of this canvas widget for purposes of user + interaction. ``_add_child_widget`` has two direct effects: + - It sets ``child``'s parent to this canvas widget. + - It adds ``child`` to the list of canvas widgets returned by + the ``child_widgets`` member function. + + :param child: The new child widget. ``child`` must not already + have a parent. + :type child: CanvasWidget + """ + if not hasattr(self, "_CanvasWidget__children"): + self.__children = [] + if child.__parent is not None: + raise ValueError(f"{child} already has a parent") + child.__parent = self + self.__children.append(child) + + def _remove_child_widget(self, child): + """ + Remove a hierarchical child widget. This child will no longer + be considered part of this canvas widget for purposes of user + interaction. ``_add_child_widget`` has two direct effects: + - It sets ``child``'s parent to None. + - It removes ``child`` from the list of canvas widgets + returned by the ``child_widgets`` member function. + + :param child: The child widget to remove. ``child`` must be a + child of this canvas widget. + :type child: CanvasWidget + """ + self.__children.remove(child) + child.__parent = None + + ##////////////////////////////////////////////////////// + ## Defined by subclass + ##////////////////////////////////////////////////////// + + @abstractmethod + def _tags(self): + """ + :return: a list of canvas tags for all graphical elements + managed by this canvas widget, not including graphical + elements managed by its child widgets. + :rtype: list of int + """ + + def _manage(self): + """ + Arrange the child widgets of this canvas widget. This method + is called when the canvas widget is initially created. It is + also called if the user calls the ``manage`` method on this + canvas widget or any of its ancestors. + + :rtype: None + """ + + def _update(self, child): + """ + Update this canvas widget in response to a change in one of + its children. + + :param child: The child that changed. + :type child: CanvasWidget + :rtype: None + """ + + +##////////////////////////////////////////////////////// +## Basic widgets. +##////////////////////////////////////////////////////// + + +class TextWidget(CanvasWidget): + """ + A canvas widget that displays a single string of text. + + Attributes: + - ``color``: the color of the text. + - ``font``: the font used to display the text. + - ``justify``: justification for multi-line texts. Valid values + are ``left``, ``center``, and ``right``. + - ``width``: the width of the text. If the text is wider than + this width, it will be line-wrapped at whitespace. + - ``draggable``: whether the text can be dragged by the user. + """ + + def __init__(self, canvas, text, **attribs): + """ + Create a new text widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :type text: str + :param text: The string of text to display. + :param attribs: The new canvas widget's attributes. + """ + self._text = text + self._tag = canvas.create_text(1, 1, text=text) + CanvasWidget.__init__(self, canvas, **attribs) + + def __setitem__(self, attr, value): + if attr in ("color", "font", "justify", "width"): + if attr == "color": + attr = "fill" + self.canvas().itemconfig(self._tag, {attr: value}) + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "width": + return int(self.canvas().itemcget(self._tag, attr)) + elif attr in ("color", "font", "justify"): + if attr == "color": + attr = "fill" + return self.canvas().itemcget(self._tag, attr) + else: + return CanvasWidget.__getitem__(self, attr) + + def _tags(self): + return [self._tag] + + def text(self): + """ + :return: The text displayed by this text widget. + :rtype: str + """ + return self.canvas().itemcget(self._tag, "TEXT") + + def set_text(self, text): + """ + Change the text that is displayed by this text widget. + + :type text: str + :param text: The string of text to display. + :rtype: None + """ + self.canvas().itemconfig(self._tag, text=text) + if self.parent() is not None: + self.parent().update(self) + + def __repr__(self): + return "[Text: %r]" % self._text + + +class SymbolWidget(TextWidget): + """ + A canvas widget that displays special symbols, such as the + negation sign and the exists operator. Symbols are specified by + name. Currently, the following symbol names are defined: ``neg``, + ``disj``, ``conj``, ``lambda``, ``merge``, ``forall``, ``exists``, + ``subseteq``, ``subset``, ``notsubset``, ``emptyset``, ``imp``, + ``rightarrow``, ``equal``, ``notequal``, ``epsilon``. + + Attributes: + + - ``color``: the color of the text. + - ``draggable``: whether the text can be dragged by the user. + + :cvar SYMBOLS: A dictionary mapping from symbols to the character + in the ``symbol`` font used to render them. + """ + + SYMBOLS = { + "neg": "\330", + "disj": "\332", + "conj": "\331", + "lambda": "\154", + "merge": "\304", + "forall": "\042", + "exists": "\044", + "subseteq": "\315", + "subset": "\314", + "notsubset": "\313", + "emptyset": "\306", + "imp": "\336", + "rightarrow": chr(222), #'\256', + "equal": "\75", + "notequal": "\271", + "intersection": "\307", + "union": "\310", + "epsilon": "e", + } + + def __init__(self, canvas, symbol, **attribs): + """ + Create a new symbol widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :type symbol: str + :param symbol: The name of the symbol to display. + :param attribs: The new canvas widget's attributes. + """ + attribs["font"] = "symbol" + TextWidget.__init__(self, canvas, "", **attribs) + self.set_symbol(symbol) + + def symbol(self): + """ + :return: the name of the symbol that is displayed by this + symbol widget. + :rtype: str + """ + return self._symbol + + def set_symbol(self, symbol): + """ + Change the symbol that is displayed by this symbol widget. + + :type symbol: str + :param symbol: The name of the symbol to display. + """ + if symbol not in SymbolWidget.SYMBOLS: + raise ValueError("Unknown symbol: %s" % symbol) + self._symbol = symbol + self.set_text(SymbolWidget.SYMBOLS[symbol]) + + def __repr__(self): + return "[Symbol: %r]" % self._symbol + + @staticmethod + def symbolsheet(size=20): + """ + Open a new Tkinter window that displays the entire alphabet + for the symbol font. This is useful for constructing the + ``SymbolWidget.SYMBOLS`` dictionary. + """ + top = Tk() + + def destroy(e, top=top): + top.destroy() + + top.bind("q", destroy) + Button(top, text="Quit", command=top.destroy).pack(side="bottom") + text = Text(top, font=("helvetica", -size), width=20, height=30) + text.pack(side="left") + sb = Scrollbar(top, command=text.yview) + text["yscrollcommand"] = sb.set + sb.pack(side="right", fill="y") + text.tag_config("symbol", font=("symbol", -size)) + for i in range(256): + if i in (0, 10): + continue # null and newline + for k, v in list(SymbolWidget.SYMBOLS.items()): + if v == chr(i): + text.insert("end", "%-10s\t" % k) + break + else: + text.insert("end", "%-10d \t" % i) + text.insert("end", "[%s]\n" % chr(i), "symbol") + top.mainloop() + + +class AbstractContainerWidget(CanvasWidget): + """ + An abstract class for canvas widgets that contain a single child, + such as ``BoxWidget`` and ``OvalWidget``. Subclasses must define + a constructor, which should create any new graphical elements and + then call the ``AbstractCanvasContainer`` constructor. Subclasses + must also define the ``_update`` method and the ``_tags`` method; + and any subclasses that define attributes should define + ``__setitem__`` and ``__getitem__``. + """ + + def __init__(self, canvas, child, **attribs): + """ + Create a new container widget. This constructor should only + be called by subclass constructors. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param child: The container's child widget. ``child`` must not + have a parent. + :type child: CanvasWidget + :param attribs: The new canvas widget's attributes. + """ + self._child = child + self._add_child_widget(child) + CanvasWidget.__init__(self, canvas, **attribs) + + def _manage(self): + self._update(self._child) + + def child(self): + """ + :return: The child widget contained by this container widget. + :rtype: CanvasWidget + """ + return self._child + + def set_child(self, child): + """ + Change the child widget contained by this container widget. + + :param child: The new child widget. ``child`` must not have a + parent. + :type child: CanvasWidget + :rtype: None + """ + self._remove_child_widget(self._child) + self._add_child_widget(child) + self._child = child + self.update(child) + + def __repr__(self): + name = self.__class__.__name__ + if name[-6:] == "Widget": + name = name[:-6] + return f"[{name}: {self._child!r}]" + + +class BoxWidget(AbstractContainerWidget): + """ + A canvas widget that places a box around a child widget. + + Attributes: + - ``fill``: The color used to fill the interior of the box. + - ``outline``: The color used to draw the outline of the box. + - ``width``: The width of the outline of the box. + - ``margin``: The number of pixels space left between the child + and the box. + - ``draggable``: whether the text can be dragged by the user. + """ + + def __init__(self, canvas, child, **attribs): + """ + Create a new box widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param child: The child widget. ``child`` must not have a + parent. + :type child: CanvasWidget + :param attribs: The new canvas widget's attributes. + """ + self._child = child + self._margin = 1 + self._box = canvas.create_rectangle(1, 1, 1, 1) + canvas.tag_lower(self._box) + AbstractContainerWidget.__init__(self, canvas, child, **attribs) + + def __setitem__(self, attr, value): + if attr == "margin": + self._margin = value + elif attr in ("outline", "fill", "width"): + self.canvas().itemconfig(self._box, {attr: value}) + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "margin": + return self._margin + elif attr == "width": + return float(self.canvas().itemcget(self._box, attr)) + elif attr in ("outline", "fill", "width"): + return self.canvas().itemcget(self._box, attr) + else: + return CanvasWidget.__getitem__(self, attr) + + def _update(self, child): + (x1, y1, x2, y2) = child.bbox() + margin = self._margin + self["width"] / 2 + self.canvas().coords( + self._box, x1 - margin, y1 - margin, x2 + margin, y2 + margin + ) + + def _tags(self): + return [self._box] + + +class OvalWidget(AbstractContainerWidget): + """ + A canvas widget that places a oval around a child widget. + + Attributes: + - ``fill``: The color used to fill the interior of the oval. + - ``outline``: The color used to draw the outline of the oval. + - ``width``: The width of the outline of the oval. + - ``margin``: The number of pixels space left between the child + and the oval. + - ``draggable``: whether the text can be dragged by the user. + - ``double``: If true, then a double-oval is drawn. + """ + + def __init__(self, canvas, child, **attribs): + """ + Create a new oval widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param child: The child widget. ``child`` must not have a + parent. + :type child: CanvasWidget + :param attribs: The new canvas widget's attributes. + """ + self._child = child + self._margin = 1 + self._oval = canvas.create_oval(1, 1, 1, 1) + self._circle = attribs.pop("circle", False) + self._double = attribs.pop("double", False) + if self._double: + self._oval2 = canvas.create_oval(1, 1, 1, 1) + else: + self._oval2 = None + canvas.tag_lower(self._oval) + AbstractContainerWidget.__init__(self, canvas, child, **attribs) + + def __setitem__(self, attr, value): + c = self.canvas() + if attr == "margin": + self._margin = value + elif attr == "double": + if value and self._oval2 is None: + # Copy attributes & position from self._oval. + x1, y1, x2, y2 = c.bbox(self._oval) + w = self["width"] * 2 + self._oval2 = c.create_oval( + x1 - w, + y1 - w, + x2 + w, + y2 + w, + outline=c.itemcget(self._oval, "outline"), + width=c.itemcget(self._oval, "width"), + ) + c.tag_lower(self._oval2) + if not value and self._oval2 is not None: + c.delete(self._oval2) + self._oval2 = None + elif attr in ("outline", "fill", "width"): + c.itemconfig(self._oval, {attr: value}) + if self._oval2 is not None and attr != "fill": + c.itemconfig(self._oval2, {attr: value}) + if self._oval2 is not None and attr != "fill": + self.canvas().itemconfig(self._oval2, {attr: value}) + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "margin": + return self._margin + elif attr == "double": + return self._double is not None + elif attr == "width": + return float(self.canvas().itemcget(self._oval, attr)) + elif attr in ("outline", "fill", "width"): + return self.canvas().itemcget(self._oval, attr) + else: + return CanvasWidget.__getitem__(self, attr) + + # The ratio between inscribed & circumscribed ovals + RATIO = 1.4142135623730949 + + def _update(self, child): + R = OvalWidget.RATIO + (x1, y1, x2, y2) = child.bbox() + margin = self._margin + + # If we're a circle, pretend our contents are square. + if self._circle: + dx, dy = abs(x1 - x2), abs(y1 - y2) + if dx > dy: + y = (y1 + y2) / 2 + y1, y2 = y - dx / 2, y + dx / 2 + elif dy > dx: + x = (x1 + x2) / 2 + x1, x2 = x - dy / 2, x + dy / 2 + + # Find the four corners. + left = int((x1 * (1 + R) + x2 * (1 - R)) / 2) + right = left + int((x2 - x1) * R) + top = int((y1 * (1 + R) + y2 * (1 - R)) / 2) + bot = top + int((y2 - y1) * R) + self.canvas().coords( + self._oval, left - margin, top - margin, right + margin, bot + margin + ) + if self._oval2 is not None: + self.canvas().coords( + self._oval2, + left - margin + 2, + top - margin + 2, + right + margin - 2, + bot + margin - 2, + ) + + def _tags(self): + if self._oval2 is None: + return [self._oval] + else: + return [self._oval, self._oval2] + + +class ParenWidget(AbstractContainerWidget): + """ + A canvas widget that places a pair of parenthases around a child + widget. + + Attributes: + - ``color``: The color used to draw the parenthases. + - ``width``: The width of the parenthases. + - ``draggable``: whether the text can be dragged by the user. + """ + + def __init__(self, canvas, child, **attribs): + """ + Create a new parenthasis widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param child: The child widget. ``child`` must not have a + parent. + :type child: CanvasWidget + :param attribs: The new canvas widget's attributes. + """ + self._child = child + self._oparen = canvas.create_arc(1, 1, 1, 1, style="arc", start=90, extent=180) + self._cparen = canvas.create_arc(1, 1, 1, 1, style="arc", start=-90, extent=180) + AbstractContainerWidget.__init__(self, canvas, child, **attribs) + + def __setitem__(self, attr, value): + if attr == "color": + self.canvas().itemconfig(self._oparen, outline=value) + self.canvas().itemconfig(self._cparen, outline=value) + elif attr == "width": + self.canvas().itemconfig(self._oparen, width=value) + self.canvas().itemconfig(self._cparen, width=value) + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "color": + return self.canvas().itemcget(self._oparen, "outline") + elif attr == "width": + return self.canvas().itemcget(self._oparen, "width") + else: + return CanvasWidget.__getitem__(self, attr) + + def _update(self, child): + (x1, y1, x2, y2) = child.bbox() + width = max((y2 - y1) / 6, 4) + self.canvas().coords(self._oparen, x1 - width, y1, x1 + width, y2) + self.canvas().coords(self._cparen, x2 - width, y1, x2 + width, y2) + + def _tags(self): + return [self._oparen, self._cparen] + + +class BracketWidget(AbstractContainerWidget): + """ + A canvas widget that places a pair of brackets around a child + widget. + + Attributes: + - ``color``: The color used to draw the brackets. + - ``width``: The width of the brackets. + - ``draggable``: whether the text can be dragged by the user. + """ + + def __init__(self, canvas, child, **attribs): + """ + Create a new bracket widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param child: The child widget. ``child`` must not have a + parent. + :type child: CanvasWidget + :param attribs: The new canvas widget's attributes. + """ + self._child = child + self._obrack = canvas.create_line(1, 1, 1, 1, 1, 1, 1, 1) + self._cbrack = canvas.create_line(1, 1, 1, 1, 1, 1, 1, 1) + AbstractContainerWidget.__init__(self, canvas, child, **attribs) + + def __setitem__(self, attr, value): + if attr == "color": + self.canvas().itemconfig(self._obrack, fill=value) + self.canvas().itemconfig(self._cbrack, fill=value) + elif attr == "width": + self.canvas().itemconfig(self._obrack, width=value) + self.canvas().itemconfig(self._cbrack, width=value) + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "color": + return self.canvas().itemcget(self._obrack, "outline") + elif attr == "width": + return self.canvas().itemcget(self._obrack, "width") + else: + return CanvasWidget.__getitem__(self, attr) + + def _update(self, child): + (x1, y1, x2, y2) = child.bbox() + width = max((y2 - y1) / 8, 2) + self.canvas().coords( + self._obrack, x1, y1, x1 - width, y1, x1 - width, y2, x1, y2 + ) + self.canvas().coords( + self._cbrack, x2, y1, x2 + width, y1, x2 + width, y2, x2, y2 + ) + + def _tags(self): + return [self._obrack, self._cbrack] + + +class SequenceWidget(CanvasWidget): + """ + A canvas widget that keeps a list of canvas widgets in a + horizontal line. + + Attributes: + - ``align``: The vertical alignment of the children. Possible + values are ``'top'``, ``'center'``, and ``'bottom'``. By + default, children are center-aligned. + - ``space``: The amount of horizontal space to place between + children. By default, one pixel of space is used. + - ``ordered``: If true, then keep the children in their + original order. + """ + + def __init__(self, canvas, *children, **attribs): + """ + Create a new sequence widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param children: The widgets that should be aligned + horizontally. Each child must not have a parent. + :type children: list(CanvasWidget) + :param attribs: The new canvas widget's attributes. + """ + self._align = "center" + self._space = 1 + self._ordered = False + self._children = list(children) + for child in children: + self._add_child_widget(child) + CanvasWidget.__init__(self, canvas, **attribs) + + def __setitem__(self, attr, value): + if attr == "align": + if value not in ("top", "bottom", "center"): + raise ValueError("Bad alignment: %r" % value) + self._align = value + elif attr == "space": + self._space = value + elif attr == "ordered": + self._ordered = value + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "align": + return self._align + elif attr == "space": + return self._space + elif attr == "ordered": + return self._ordered + else: + return CanvasWidget.__getitem__(self, attr) + + def _tags(self): + return [] + + def _yalign(self, top, bot): + if self._align == "top": + return top + if self._align == "bottom": + return bot + if self._align == "center": + return (top + bot) / 2 + + def _update(self, child): + # Align all children with child. + (left, top, right, bot) = child.bbox() + y = self._yalign(top, bot) + for c in self._children: + (x1, y1, x2, y2) = c.bbox() + c.move(0, y - self._yalign(y1, y2)) + + if self._ordered and len(self._children) > 1: + index = self._children.index(child) + + x = right + self._space + for i in range(index + 1, len(self._children)): + (x1, y1, x2, y2) = self._children[i].bbox() + if x > x1: + self._children[i].move(x - x1, 0) + x += x2 - x1 + self._space + + x = left - self._space + for i in range(index - 1, -1, -1): + (x1, y1, x2, y2) = self._children[i].bbox() + if x < x2: + self._children[i].move(x - x2, 0) + x -= x2 - x1 + self._space + + def _manage(self): + if len(self._children) == 0: + return + child = self._children[0] + + # Align all children with child. + (left, top, right, bot) = child.bbox() + y = self._yalign(top, bot) + + index = self._children.index(child) + + # Line up children to the right of child. + x = right + self._space + for i in range(index + 1, len(self._children)): + (x1, y1, x2, y2) = self._children[i].bbox() + self._children[i].move(x - x1, y - self._yalign(y1, y2)) + x += x2 - x1 + self._space + + # Line up children to the left of child. + x = left - self._space + for i in range(index - 1, -1, -1): + (x1, y1, x2, y2) = self._children[i].bbox() + self._children[i].move(x - x2, y - self._yalign(y1, y2)) + x -= x2 - x1 + self._space + + def __repr__(self): + return "[Sequence: " + repr(self._children)[1:-1] + "]" + + # Provide an alias for the child_widgets() member. + children = CanvasWidget.child_widgets + + def replace_child(self, oldchild, newchild): + """ + Replace the child canvas widget ``oldchild`` with ``newchild``. + ``newchild`` must not have a parent. ``oldchild``'s parent will + be set to None. + + :type oldchild: CanvasWidget + :param oldchild: The child canvas widget to remove. + :type newchild: CanvasWidget + :param newchild: The canvas widget that should replace + ``oldchild``. + """ + index = self._children.index(oldchild) + self._children[index] = newchild + self._remove_child_widget(oldchild) + self._add_child_widget(newchild) + self.update(newchild) + + def remove_child(self, child): + """ + Remove the given child canvas widget. ``child``'s parent will + be set to None. + + :type child: CanvasWidget + :param child: The child canvas widget to remove. + """ + index = self._children.index(child) + del self._children[index] + self._remove_child_widget(child) + if len(self._children) > 0: + self.update(self._children[0]) + + def insert_child(self, index, child): + """ + Insert a child canvas widget before a given index. + + :type child: CanvasWidget + :param child: The canvas widget that should be inserted. + :type index: int + :param index: The index where the child widget should be + inserted. In particular, the index of ``child`` will be + ``index``; and the index of any children whose indices were + greater than equal to ``index`` before ``child`` was + inserted will be incremented by one. + """ + self._children.insert(index, child) + self._add_child_widget(child) + + +class StackWidget(CanvasWidget): + """ + A canvas widget that keeps a list of canvas widgets in a vertical + line. + + Attributes: + - ``align``: The horizontal alignment of the children. Possible + values are ``'left'``, ``'center'``, and ``'right'``. By + default, children are center-aligned. + - ``space``: The amount of vertical space to place between + children. By default, one pixel of space is used. + - ``ordered``: If true, then keep the children in their + original order. + """ + + def __init__(self, canvas, *children, **attribs): + """ + Create a new stack widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :param children: The widgets that should be aligned + vertically. Each child must not have a parent. + :type children: list(CanvasWidget) + :param attribs: The new canvas widget's attributes. + """ + self._align = "center" + self._space = 1 + self._ordered = False + self._children = list(children) + for child in children: + self._add_child_widget(child) + CanvasWidget.__init__(self, canvas, **attribs) + + def __setitem__(self, attr, value): + if attr == "align": + if value not in ("left", "right", "center"): + raise ValueError("Bad alignment: %r" % value) + self._align = value + elif attr == "space": + self._space = value + elif attr == "ordered": + self._ordered = value + else: + CanvasWidget.__setitem__(self, attr, value) + + def __getitem__(self, attr): + if attr == "align": + return self._align + elif attr == "space": + return self._space + elif attr == "ordered": + return self._ordered + else: + return CanvasWidget.__getitem__(self, attr) + + def _tags(self): + return [] + + def _xalign(self, left, right): + if self._align == "left": + return left + if self._align == "right": + return right + if self._align == "center": + return (left + right) / 2 + + def _update(self, child): + # Align all children with child. + (left, top, right, bot) = child.bbox() + x = self._xalign(left, right) + for c in self._children: + (x1, y1, x2, y2) = c.bbox() + c.move(x - self._xalign(x1, x2), 0) + + if self._ordered and len(self._children) > 1: + index = self._children.index(child) + + y = bot + self._space + for i in range(index + 1, len(self._children)): + (x1, y1, x2, y2) = self._children[i].bbox() + if y > y1: + self._children[i].move(0, y - y1) + y += y2 - y1 + self._space + + y = top - self._space + for i in range(index - 1, -1, -1): + (x1, y1, x2, y2) = self._children[i].bbox() + if y < y2: + self._children[i].move(0, y - y2) + y -= y2 - y1 + self._space + + def _manage(self): + if len(self._children) == 0: + return + child = self._children[0] + + # Align all children with child. + (left, top, right, bot) = child.bbox() + x = self._xalign(left, right) + + index = self._children.index(child) + + # Line up children below the child. + y = bot + self._space + for i in range(index + 1, len(self._children)): + (x1, y1, x2, y2) = self._children[i].bbox() + self._children[i].move(x - self._xalign(x1, x2), y - y1) + y += y2 - y1 + self._space + + # Line up children above the child. + y = top - self._space + for i in range(index - 1, -1, -1): + (x1, y1, x2, y2) = self._children[i].bbox() + self._children[i].move(x - self._xalign(x1, x2), y - y2) + y -= y2 - y1 + self._space + + def __repr__(self): + return "[Stack: " + repr(self._children)[1:-1] + "]" + + # Provide an alias for the child_widgets() member. + children = CanvasWidget.child_widgets + + def replace_child(self, oldchild, newchild): + """ + Replace the child canvas widget ``oldchild`` with ``newchild``. + ``newchild`` must not have a parent. ``oldchild``'s parent will + be set to None. + + :type oldchild: CanvasWidget + :param oldchild: The child canvas widget to remove. + :type newchild: CanvasWidget + :param newchild: The canvas widget that should replace + ``oldchild``. + """ + index = self._children.index(oldchild) + self._children[index] = newchild + self._remove_child_widget(oldchild) + self._add_child_widget(newchild) + self.update(newchild) + + def remove_child(self, child): + """ + Remove the given child canvas widget. ``child``'s parent will + be set to None. + + :type child: CanvasWidget + :param child: The child canvas widget to remove. + """ + index = self._children.index(child) + del self._children[index] + self._remove_child_widget(child) + if len(self._children) > 0: + self.update(self._children[0]) + + def insert_child(self, index, child): + """ + Insert a child canvas widget before a given index. + + :type child: CanvasWidget + :param child: The canvas widget that should be inserted. + :type index: int + :param index: The index where the child widget should be + inserted. In particular, the index of ``child`` will be + ``index``; and the index of any children whose indices were + greater than equal to ``index`` before ``child`` was + inserted will be incremented by one. + """ + self._children.insert(index, child) + self._add_child_widget(child) + + +class SpaceWidget(CanvasWidget): + """ + A canvas widget that takes up space but does not display + anything. A ``SpaceWidget`` can be used to add space between + elements. Each space widget is characterized by a width and a + height. If you wish to only create horizontal space, then use a + height of zero; and if you wish to only create vertical space, use + a width of zero. + """ + + def __init__(self, canvas, width, height, **attribs): + """ + Create a new space widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :type width: int + :param width: The width of the new space widget. + :type height: int + :param height: The height of the new space widget. + :param attribs: The new canvas widget's attributes. + """ + # For some reason, + if width > 4: + width -= 4 + if height > 4: + height -= 4 + self._tag = canvas.create_line(1, 1, width, height, fill="") + CanvasWidget.__init__(self, canvas, **attribs) + + # note: width() and height() are already defined by CanvasWidget. + def set_width(self, width): + """ + Change the width of this space widget. + + :param width: The new width. + :type width: int + :rtype: None + """ + [x1, y1, x2, y2] = self.bbox() + self.canvas().coords(self._tag, x1, y1, x1 + width, y2) + + def set_height(self, height): + """ + Change the height of this space widget. + + :param height: The new height. + :type height: int + :rtype: None + """ + [x1, y1, x2, y2] = self.bbox() + self.canvas().coords(self._tag, x1, y1, x2, y1 + height) + + def _tags(self): + return [self._tag] + + def __repr__(self): + return "[Space]" + + +class ScrollWatcherWidget(CanvasWidget): + """ + A special canvas widget that adjusts its ``Canvas``'s scrollregion + to always include the bounding boxes of all of its children. The + scroll-watcher widget will only increase the size of the + ``Canvas``'s scrollregion; it will never decrease it. + """ + + def __init__(self, canvas, *children, **attribs): + """ + Create a new scroll-watcher widget. + + :type canvas: Tkinter.Canvas + :param canvas: This canvas widget's canvas. + :type children: list(CanvasWidget) + :param children: The canvas widgets watched by the + scroll-watcher. The scroll-watcher will ensure that these + canvas widgets are always contained in their canvas's + scrollregion. + :param attribs: The new canvas widget's attributes. + """ + for child in children: + self._add_child_widget(child) + CanvasWidget.__init__(self, canvas, **attribs) + + def add_child(self, canvaswidget): + """ + Add a new canvas widget to the scroll-watcher. The + scroll-watcher will ensure that the new canvas widget is + always contained in its canvas's scrollregion. + + :param canvaswidget: The new canvas widget. + :type canvaswidget: CanvasWidget + :rtype: None + """ + self._add_child_widget(canvaswidget) + self.update(canvaswidget) + + def remove_child(self, canvaswidget): + """ + Remove a canvas widget from the scroll-watcher. The + scroll-watcher will no longer ensure that the new canvas + widget is always contained in its canvas's scrollregion. + + :param canvaswidget: The canvas widget to remove. + :type canvaswidget: CanvasWidget + :rtype: None + """ + self._remove_child_widget(canvaswidget) + + def _tags(self): + return [] + + def _update(self, child): + self._adjust_scrollregion() + + def _adjust_scrollregion(self): + """ + Adjust the scrollregion of this scroll-watcher's ``Canvas`` to + include the bounding boxes of all of its children. + """ + bbox = self.bbox() + canvas = self.canvas() + scrollregion = [int(n) for n in canvas["scrollregion"].split()] + if len(scrollregion) != 4: + return + if ( + bbox[0] < scrollregion[0] + or bbox[1] < scrollregion[1] + or bbox[2] > scrollregion[2] + or bbox[3] > scrollregion[3] + ): + scrollregion = "%d %d %d %d" % ( + min(bbox[0], scrollregion[0]), + min(bbox[1], scrollregion[1]), + max(bbox[2], scrollregion[2]), + max(bbox[3], scrollregion[3]), + ) + canvas["scrollregion"] = scrollregion + + +##////////////////////////////////////////////////////// +## Canvas Frame +##////////////////////////////////////////////////////// + + +class CanvasFrame: + """ + A ``Tkinter`` frame containing a canvas and scrollbars. + ``CanvasFrame`` uses a ``ScrollWatcherWidget`` to ensure that all of + the canvas widgets contained on its canvas are within its + scrollregion. In order for ``CanvasFrame`` to make these checks, + all canvas widgets must be registered with ``add_widget`` when they + are added to the canvas; and destroyed with ``destroy_widget`` when + they are no longer needed. + + If a ``CanvasFrame`` is created with no parent, then it will create + its own main window, including a "Done" button and a "Print" + button. + """ + + def __init__(self, parent=None, **kw): + """ + Create a new ``CanvasFrame``. + + :type parent: Tkinter.BaseWidget or Tkinter.Tk + :param parent: The parent ``Tkinter`` widget. If no parent is + specified, then ``CanvasFrame`` will create a new main + window. + :param kw: Keyword arguments for the new ``Canvas``. See the + documentation for ``Tkinter.Canvas`` for more information. + """ + # If no parent was given, set up a top-level window. + if parent is None: + self._parent = Tk() + self._parent.title("NLTK") + self._parent.bind("", lambda e: self.print_to_file()) + self._parent.bind("", self.destroy) + self._parent.bind("", self.destroy) + else: + self._parent = parent + + # Create a frame for the canvas & scrollbars + self._frame = frame = Frame(self._parent) + self._canvas = canvas = Canvas(frame, **kw) + xscrollbar = Scrollbar(self._frame, orient="horizontal") + yscrollbar = Scrollbar(self._frame, orient="vertical") + xscrollbar["command"] = canvas.xview + yscrollbar["command"] = canvas.yview + canvas["xscrollcommand"] = xscrollbar.set + canvas["yscrollcommand"] = yscrollbar.set + yscrollbar.pack(fill="y", side="right") + xscrollbar.pack(fill="x", side="bottom") + canvas.pack(expand=1, fill="both", side="left") + + # Set initial scroll region. + scrollregion = "0 0 {} {}".format(canvas["width"], canvas["height"]) + canvas["scrollregion"] = scrollregion + + self._scrollwatcher = ScrollWatcherWidget(canvas) + + # If no parent was given, pack the frame, and add a menu. + if parent is None: + self.pack(expand=1, fill="both") + self._init_menubar() + + def _init_menubar(self): + menubar = Menu(self._parent) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Print to Postscript", + underline=0, + command=self.print_to_file, + accelerator="Ctrl-p", + ) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="Ctrl-x" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + self._parent.config(menu=menubar) + + def print_to_file(self, filename=None): + """ + Print the contents of this ``CanvasFrame`` to a postscript + file. If no filename is given, then prompt the user for one. + + :param filename: The name of the file to print the tree to. + :type filename: str + :rtype: None + """ + if filename is None: + ftypes = [("Postscript files", ".ps"), ("All files", "*")] + filename = asksaveasfilename(filetypes=ftypes, defaultextension=".ps") + if not filename: + return + (x0, y0, w, h) = self.scrollregion() + postscript = self._canvas.postscript( + x=x0, + y=y0, + width=w + 2, + height=h + 2, + pagewidth=w + 2, # points = 1/72 inch + pageheight=h + 2, # points = 1/72 inch + pagex=0, + pagey=0, + ) + # workaround for bug in Tk font handling + postscript = postscript.replace(" 0 scalefont ", " 9 scalefont ") + with open(filename, "wb") as f: + f.write(postscript.encode("utf8")) + + def scrollregion(self): + """ + :return: The current scroll region for the canvas managed by + this ``CanvasFrame``. + :rtype: 4-tuple of int + """ + (x1, y1, x2, y2) = self._canvas["scrollregion"].split() + return (int(x1), int(y1), int(x2), int(y2)) + + def canvas(self): + """ + :return: The canvas managed by this ``CanvasFrame``. + :rtype: Tkinter.Canvas + """ + return self._canvas + + def add_widget(self, canvaswidget, x=None, y=None): + """ + Register a canvas widget with this ``CanvasFrame``. The + ``CanvasFrame`` will ensure that this canvas widget is always + within the ``Canvas``'s scrollregion. If no coordinates are + given for the canvas widget, then the ``CanvasFrame`` will + attempt to find a clear area of the canvas for it. + + :type canvaswidget: CanvasWidget + :param canvaswidget: The new canvas widget. ``canvaswidget`` + must have been created on this ``CanvasFrame``'s canvas. + :type x: int + :param x: The initial x coordinate for the upper left hand + corner of ``canvaswidget``, in the canvas's coordinate + space. + :type y: int + :param y: The initial y coordinate for the upper left hand + corner of ``canvaswidget``, in the canvas's coordinate + space. + """ + if x is None or y is None: + (x, y) = self._find_room(canvaswidget, x, y) + + # Move to (x,y) + (x1, y1, x2, y2) = canvaswidget.bbox() + canvaswidget.move(x - x1, y - y1) + + # Register with scrollwatcher. + self._scrollwatcher.add_child(canvaswidget) + + def _find_room(self, widget, desired_x, desired_y): + """ + Try to find a space for a given widget. + """ + (left, top, right, bot) = self.scrollregion() + w = widget.width() + h = widget.height() + + if w >= (right - left): + return (0, 0) + if h >= (bot - top): + return (0, 0) + + # Move the widget out of the way, for now. + (x1, y1, x2, y2) = widget.bbox() + widget.move(left - x2 - 50, top - y2 - 50) + + if desired_x is not None: + x = desired_x + for y in range(top, bot - h, int((bot - top - h) / 10)): + if not self._canvas.find_overlapping( + x - 5, y - 5, x + w + 5, y + h + 5 + ): + return (x, y) + + if desired_y is not None: + y = desired_y + for x in range(left, right - w, int((right - left - w) / 10)): + if not self._canvas.find_overlapping( + x - 5, y - 5, x + w + 5, y + h + 5 + ): + return (x, y) + + for y in range(top, bot - h, int((bot - top - h) / 10)): + for x in range(left, right - w, int((right - left - w) / 10)): + if not self._canvas.find_overlapping( + x - 5, y - 5, x + w + 5, y + h + 5 + ): + return (x, y) + return (0, 0) + + def destroy_widget(self, canvaswidget): + """ + Remove a canvas widget from this ``CanvasFrame``. This + deregisters the canvas widget, and destroys it. + """ + self.remove_widget(canvaswidget) + canvaswidget.destroy() + + def remove_widget(self, canvaswidget): + # Deregister with scrollwatcher. + self._scrollwatcher.remove_child(canvaswidget) + + def pack(self, cnf={}, **kw): + """ + Pack this ``CanvasFrame``. See the documentation for + ``Tkinter.Pack`` for more information. + """ + self._frame.pack(cnf, **kw) + # Adjust to be big enough for kids? + + def destroy(self, *e): + """ + Destroy this ``CanvasFrame``. If this ``CanvasFrame`` created a + top-level window, then this will close that window. + """ + if self._parent is None: + return + self._parent.destroy() + self._parent = None + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this frame is created from a non-interactive program (e.g. + from a secript); otherwise, the frame will close as soon as + the script completes. + """ + if in_idle(): + return + self._parent.mainloop(*args, **kwargs) + + +##////////////////////////////////////////////////////// +## Text display +##////////////////////////////////////////////////////// + + +class ShowText: + """ + A ``Tkinter`` window used to display a text. ``ShowText`` is + typically used by graphical tools to display help text, or similar + information. + """ + + def __init__(self, root, title, text, width=None, height=None, **textbox_options): + if width is None or height is None: + (width, height) = self.find_dimentions(text, width, height) + + # Create the main window. + if root is None: + self._top = top = Tk() + else: + self._top = top = Toplevel(root) + top.title(title) + + b = Button(top, text="Ok", command=self.destroy) + b.pack(side="bottom") + + tbf = Frame(top) + tbf.pack(expand=1, fill="both") + scrollbar = Scrollbar(tbf, orient="vertical") + scrollbar.pack(side="right", fill="y") + textbox = Text(tbf, wrap="word", width=width, height=height, **textbox_options) + textbox.insert("end", text) + textbox["state"] = "disabled" + textbox.pack(side="left", expand=1, fill="both") + scrollbar["command"] = textbox.yview + textbox["yscrollcommand"] = scrollbar.set + + # Make it easy to close the window. + top.bind("q", self.destroy) + top.bind("x", self.destroy) + top.bind("c", self.destroy) + top.bind("", self.destroy) + top.bind("", self.destroy) + + # Focus the scrollbar, so they can use up/down, etc. + scrollbar.focus() + + def find_dimentions(self, text, width, height): + lines = text.split("\n") + if width is None: + maxwidth = max(len(line) for line in lines) + width = min(maxwidth, 80) + + # Now, find height. + height = 0 + for line in lines: + while len(line) > width: + brk = line[:width].rfind(" ") + line = line[brk:] + height += 1 + height += 1 + height = min(height, 25) + + return (width, height) + + def destroy(self, *e): + if self._top is None: + return + self._top.destroy() + self._top = None + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this window is created from a non-interactive program (e.g. + from a secript); otherwise, the window will close as soon as + the script completes. + """ + if in_idle(): + return + self._top.mainloop(*args, **kwargs) + + +##////////////////////////////////////////////////////// +## Entry dialog +##////////////////////////////////////////////////////// + + +class EntryDialog: + """ + A dialog box for entering + """ + + def __init__( + self, parent, original_text="", instructions="", set_callback=None, title=None + ): + self._parent = parent + self._original_text = original_text + self._set_callback = set_callback + + width = int(max(30, len(original_text) * 3 / 2)) + self._top = Toplevel(parent) + + if title: + self._top.title(title) + + # The text entry box. + entryframe = Frame(self._top) + entryframe.pack(expand=1, fill="both", padx=5, pady=5, ipady=10) + if instructions: + l = Label(entryframe, text=instructions) + l.pack(side="top", anchor="w", padx=30) + self._entry = Entry(entryframe, width=width) + self._entry.pack(expand=1, fill="x", padx=30) + self._entry.insert(0, original_text) + + # A divider + divider = Frame(self._top, borderwidth=1, relief="sunken") + divider.pack(fill="x", ipady=1, padx=10) + + # The buttons. + buttons = Frame(self._top) + buttons.pack(expand=0, fill="x", padx=5, pady=5) + b = Button(buttons, text="Cancel", command=self._cancel, width=8) + b.pack(side="right", padx=5) + b = Button(buttons, text="Ok", command=self._ok, width=8, default="active") + b.pack(side="left", padx=5) + b = Button(buttons, text="Apply", command=self._apply, width=8) + b.pack(side="left") + + self._top.bind("", self._ok) + self._top.bind("", self._cancel) + self._top.bind("", self._cancel) + + self._entry.focus() + + def _reset(self, *e): + self._entry.delete(0, "end") + self._entry.insert(0, self._original_text) + if self._set_callback: + self._set_callback(self._original_text) + + def _cancel(self, *e): + try: + self._reset() + except Exception: + pass + self._destroy() + + def _ok(self, *e): + self._apply() + self._destroy() + + def _apply(self, *e): + if self._set_callback: + self._set_callback(self._entry.get()) + + def _destroy(self, *e): + if self._top is None: + return + self._top.destroy() + self._top = None + + +##////////////////////////////////////////////////////// +## Colorized List +##////////////////////////////////////////////////////// + + +class ColorizedList: + """ + An abstract base class for displaying a colorized list of items. + Subclasses should define: + + - ``_init_colortags``, which sets up Text color tags that + will be used by the list. + - ``_item_repr``, which returns a list of (text,colortag) + tuples that make up the colorized representation of the + item. + + :note: Typically, you will want to register a callback for + ``'select'`` that calls ``mark`` on the given item. + """ + + def __init__(self, parent, items=[], **options): + """ + Construct a new list. + + :param parent: The Tk widget that contains the colorized list + :param items: The initial contents of the colorized list. + :param options: + """ + self._parent = parent + self._callbacks = {} + + # Which items are marked? + self._marks = {} + + # Initialize the Tkinter frames. + self._init_itemframe(options.copy()) + + # Set up key & mouse bindings. + self._textwidget.bind("", self._keypress) + self._textwidget.bind("", self._buttonpress) + + # Fill in the given CFG's items. + self._items = None + self.set(items) + + # //////////////////////////////////////////////////////////// + # Abstract methods + # //////////////////////////////////////////////////////////// + @abstractmethod + def _init_colortags(self, textwidget, options): + """ + Set up any colortags that will be used by this colorized list. + E.g.: + textwidget.tag_config('terminal', foreground='black') + """ + + @abstractmethod + def _item_repr(self, item): + """ + Return a list of (text, colortag) tuples that make up the + colorized representation of the item. Colorized + representations may not span multiple lines. I.e., the text + strings returned may not contain newline characters. + """ + + # //////////////////////////////////////////////////////////// + # Item Access + # //////////////////////////////////////////////////////////// + + def get(self, index=None): + """ + :return: A list of the items contained by this list. + """ + if index is None: + return self._items[:] + else: + return self._items[index] + + def set(self, items): + """ + Modify the list of items contained by this list. + """ + items = list(items) + if self._items == items: + return + self._items = list(items) + + self._textwidget["state"] = "normal" + self._textwidget.delete("1.0", "end") + for item in items: + for text, colortag in self._item_repr(item): + assert "\n" not in text, "item repr may not contain newline" + self._textwidget.insert("end", text, colortag) + self._textwidget.insert("end", "\n") + # Remove the final newline + self._textwidget.delete("end-1char", "end") + self._textwidget.mark_set("insert", "1.0") + self._textwidget["state"] = "disabled" + # Clear all marks + self._marks.clear() + + def unmark(self, item=None): + """ + Remove highlighting from the given item; or from every item, + if no item is given. + :raise ValueError: If ``item`` is not contained in the list. + :raise KeyError: If ``item`` is not marked. + """ + if item is None: + self._marks.clear() + self._textwidget.tag_remove("highlight", "1.0", "end+1char") + else: + index = self._items.index(item) + del self._marks[item] + (start, end) = ("%d.0" % (index + 1), "%d.0" % (index + 2)) + self._textwidget.tag_remove("highlight", start, end) + + def mark(self, item): + """ + Highlight the given item. + :raise ValueError: If ``item`` is not contained in the list. + """ + self._marks[item] = 1 + index = self._items.index(item) + (start, end) = ("%d.0" % (index + 1), "%d.0" % (index + 2)) + self._textwidget.tag_add("highlight", start, end) + + def markonly(self, item): + """ + Remove any current highlighting, and mark the given item. + :raise ValueError: If ``item`` is not contained in the list. + """ + self.unmark() + self.mark(item) + + def view(self, item): + """ + Adjust the view such that the given item is visible. If + the item is already visible, then do nothing. + """ + index = self._items.index(item) + self._textwidget.see("%d.0" % (index + 1)) + + # //////////////////////////////////////////////////////////// + # Callbacks + # //////////////////////////////////////////////////////////// + + def add_callback(self, event, func): + """ + Register a callback function with the list. This function + will be called whenever the given event occurs. + + :param event: The event that will trigger the callback + function. Valid events are: click1, click2, click3, + space, return, select, up, down, next, prior, move + :param func: The function that should be called when + the event occurs. ``func`` will be called with a + single item as its argument. (The item selected + or the item moved to). + """ + if event == "select": + events = ["click1", "space", "return"] + elif event == "move": + events = ["up", "down", "next", "prior"] + else: + events = [event] + + for e in events: + self._callbacks.setdefault(e, {})[func] = 1 + + def remove_callback(self, event, func=None): + """ + Deregister a callback function. If ``func`` is none, then + all callbacks are removed for the given event. + """ + if event is None: + events = list(self._callbacks.keys()) + elif event == "select": + events = ["click1", "space", "return"] + elif event == "move": + events = ["up", "down", "next", "prior"] + else: + events = [event] + + for e in events: + if func is None: + del self._callbacks[e] + else: + try: + del self._callbacks[e][func] + except KeyError: + pass + + # //////////////////////////////////////////////////////////// + # Tkinter Methods + # //////////////////////////////////////////////////////////// + + def pack(self, cnf={}, **kw): + # "@include: Tkinter.Pack.pack" + self._itemframe.pack(cnf, **kw) + + def grid(self, cnf={}, **kw): + # "@include: Tkinter.Grid.grid" + self._itemframe.grid(cnf, *kw) + + def focus(self): + # "@include: Tkinter.Widget.focus" + self._textwidget.focus() + + # //////////////////////////////////////////////////////////// + # Internal Methods + # //////////////////////////////////////////////////////////// + + def _init_itemframe(self, options): + self._itemframe = Frame(self._parent) + + # Create the basic Text widget & scrollbar. + options.setdefault("background", "#e0e0e0") + self._textwidget = Text(self._itemframe, **options) + self._textscroll = Scrollbar(self._itemframe, takefocus=0, orient="vertical") + self._textwidget.config(yscrollcommand=self._textscroll.set) + self._textscroll.config(command=self._textwidget.yview) + self._textscroll.pack(side="right", fill="y") + self._textwidget.pack(expand=1, fill="both", side="left") + + # Initialize the colorization tags + self._textwidget.tag_config( + "highlight", background="#e0ffff", border="1", relief="raised" + ) + self._init_colortags(self._textwidget, options) + + # How do I want to mark keyboard selection? + self._textwidget.tag_config("sel", foreground="") + self._textwidget.tag_config( + "sel", foreground="", background="", border="", underline=1 + ) + self._textwidget.tag_lower("highlight", "sel") + + def _fire_callback(self, event, itemnum): + if event not in self._callbacks: + return + if 0 <= itemnum < len(self._items): + item = self._items[itemnum] + else: + item = None + for cb_func in list(self._callbacks[event].keys()): + cb_func(item) + + def _buttonpress(self, event): + clickloc = "@%d,%d" % (event.x, event.y) + insert_point = self._textwidget.index(clickloc) + itemnum = int(insert_point.split(".")[0]) - 1 + self._fire_callback("click%d" % event.num, itemnum) + + def _keypress(self, event): + if event.keysym == "Return" or event.keysym == "space": + insert_point = self._textwidget.index("insert") + itemnum = int(insert_point.split(".")[0]) - 1 + self._fire_callback(event.keysym.lower(), itemnum) + return + elif event.keysym == "Down": + delta = "+1line" + elif event.keysym == "Up": + delta = "-1line" + elif event.keysym == "Next": + delta = "+10lines" + elif event.keysym == "Prior": + delta = "-10lines" + else: + return "continue" + + self._textwidget.mark_set("insert", "insert" + delta) + self._textwidget.see("insert") + self._textwidget.tag_remove("sel", "1.0", "end+1char") + self._textwidget.tag_add("sel", "insert linestart", "insert lineend") + + insert_point = self._textwidget.index("insert") + itemnum = int(insert_point.split(".")[0]) - 1 + self._fire_callback(event.keysym.lower(), itemnum) + + return "break" + + +##////////////////////////////////////////////////////// +## Improved OptionMenu +##////////////////////////////////////////////////////// + + +class MutableOptionMenu(Menubutton): + def __init__(self, master, values, **options): + self._callback = options.get("command") + if "command" in options: + del options["command"] + + # Create a variable + self._variable = variable = StringVar() + if len(values) > 0: + variable.set(values[0]) + + kw = { + "borderwidth": 2, + "textvariable": variable, + "indicatoron": 1, + "relief": RAISED, + "anchor": "c", + "highlightthickness": 2, + } + kw.update(options) + Widget.__init__(self, master, "menubutton", kw) + self.widgetName = "tk_optionMenu" + self._menu = Menu(self, name="menu", tearoff=0) + self.menuname = self._menu._w + + self._values = [] + for value in values: + self.add(value) + + self["menu"] = self._menu + + def add(self, value): + if value in self._values: + return + + def set(value=value): + self.set(value) + + self._menu.add_command(label=value, command=set) + self._values.append(value) + + def set(self, value): + self._variable.set(value) + if self._callback: + self._callback(value) + + def remove(self, value): + # Might raise indexerror: pass to parent. + i = self._values.index(value) + del self._values[i] + self._menu.delete(i, i) + + def __getitem__(self, name): + if name == "menu": + return self.__menu + return Widget.__getitem__(self, name) + + def destroy(self): + """Destroy this widget and the associated menu.""" + Menubutton.destroy(self) + self._menu = None + + +##////////////////////////////////////////////////////// +## Test code. +##////////////////////////////////////////////////////// + + +def demo(): + """ + A simple demonstration showing how to use canvas widgets. + """ + + def fill(cw): + from random import randint + + cw["fill"] = "#00%04d" % randint(0, 9999) + + def color(cw): + from random import randint + + cw["color"] = "#ff%04d" % randint(0, 9999) + + cf = CanvasFrame(closeenough=10, width=300, height=300) + c = cf.canvas() + ct3 = TextWidget(c, "hiya there", draggable=1) + ct2 = TextWidget(c, "o o\n||\n___\n U", draggable=1, justify="center") + co = OvalWidget(c, ct2, outline="red") + ct = TextWidget(c, "o o\n||\n\\___/", draggable=1, justify="center") + cp = ParenWidget(c, ct, color="red") + cb = BoxWidget(c, cp, fill="cyan", draggable=1, width=3, margin=10) + equation = SequenceWidget( + c, + SymbolWidget(c, "forall"), + TextWidget(c, "x"), + SymbolWidget(c, "exists"), + TextWidget(c, "y: "), + TextWidget(c, "x"), + SymbolWidget(c, "notequal"), + TextWidget(c, "y"), + ) + space = SpaceWidget(c, 0, 30) + cstack = StackWidget(c, cb, ct3, space, co, equation, align="center") + prompt_msg = TextWidget( + c, "try clicking\nand dragging", draggable=1, justify="center" + ) + cs = SequenceWidget(c, cstack, prompt_msg) + zz = BracketWidget(c, cs, color="green4", width=3) + cf.add_widget(zz, 60, 30) + + cb.bind_click(fill) + ct.bind_click(color) + co.bind_click(fill) + ct2.bind_click(color) + ct3.bind_click(color) + + cf.mainloop() + # ShowText(None, 'title', ((('this is text'*150)+'\n')*5)) + + +if __name__ == "__main__": + demo() diff --git a/nltk/featstruct.py b/nltk/featstruct.py new file mode 100644 index 0000000..1ab9f90 --- /dev/null +++ b/nltk/featstruct.py @@ -0,0 +1,2787 @@ +# Natural Language Toolkit: Feature Structures +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper , +# Rob Speer, +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Basic data classes for representing feature structures, and for +performing basic operations on those feature structures. A feature +structure is a mapping from feature identifiers to feature values, +where each feature value is either a basic value (such as a string or +an integer), or a nested feature structure. There are two types of +feature structure, implemented by two subclasses of ``FeatStruct``: + + - feature dictionaries, implemented by ``FeatDict``, act like + Python dictionaries. Feature identifiers may be strings or + instances of the ``Feature`` class. + - feature lists, implemented by ``FeatList``, act like Python + lists. Feature identifiers are integers. + +Feature structures are typically used to represent partial information +about objects. A feature identifier that is not mapped to a value +stands for a feature whose value is unknown (*not* a feature without +a value). Two feature structures that represent (potentially +overlapping) information about the same object can be combined by +unification. When two inconsistent feature structures are unified, +the unification fails and returns None. + +Features can be specified using "feature paths", or tuples of feature +identifiers that specify path through the nested feature structures to +a value. Feature structures may contain reentrant feature values. A +"reentrant feature value" is a single feature value that can be +accessed via multiple feature paths. Unification preserves the +reentrance relations imposed by both of the unified feature +structures. In the feature structure resulting from unification, any +modifications to a reentrant feature value will be visible using any +of its feature paths. + +Feature structure variables are encoded using the ``nltk.sem.Variable`` +class. The variables' values are tracked using a bindings +dictionary, which maps variables to their values. When two feature +structures are unified, a fresh bindings dictionary is created to +track their values; and before unification completes, all bound +variables are replaced by their values. Thus, the bindings +dictionaries are usually strictly internal to the unification process. +However, it is possible to track the bindings of variables if you +choose to, by supplying your own initial bindings dictionary to the +``unify()`` function. + +When unbound variables are unified with one another, they become +aliased. This is encoded by binding one variable to the other. + +Lightweight Feature Structures +============================== +Many of the functions defined by ``nltk.featstruct`` can be applied +directly to simple Python dictionaries and lists, rather than to +full-fledged ``FeatDict`` and ``FeatList`` objects. In other words, +Python ``dicts`` and ``lists`` can be used as "light-weight" feature +structures. + + >>> from nltk.featstruct import unify + >>> unify(dict(x=1, y=dict()), dict(a='a', y=dict(b='b'))) # doctest: +SKIP + {'y': {'b': 'b'}, 'x': 1, 'a': 'a'} + +However, you should keep in mind the following caveats: + + - Python dictionaries & lists ignore reentrance when checking for + equality between values. But two FeatStructs with different + reentrances are considered nonequal, even if all their base + values are equal. + + - FeatStructs can be easily frozen, allowing them to be used as + keys in hash tables. Python dictionaries and lists can not. + + - FeatStructs display reentrance in their string representations; + Python dictionaries and lists do not. + + - FeatStructs may *not* be mixed with Python dictionaries and lists + (e.g., when performing unification). + + - FeatStructs provide a number of useful methods, such as ``walk()`` + and ``cyclic()``, which are not available for Python dicts and lists. + +In general, if your feature structures will contain any reentrances, +or if you plan to use them as dictionary keys, it is strongly +recommended that you use full-fledged ``FeatStruct`` objects. +""" + +import copy +import re +from functools import total_ordering + +from nltk.internals import raise_unorderable_types, read_str +from nltk.sem.logic import ( + Expression, + LogicalExpressionException, + LogicParser, + SubstituteBindingsI, + Variable, +) + +###################################################################### +# Feature Structure +###################################################################### + + +@total_ordering +class FeatStruct(SubstituteBindingsI): + """ + A mapping from feature identifiers to feature values, where each + feature value is either a basic value (such as a string or an + integer), or a nested feature structure. There are two types of + feature structure: + + - feature dictionaries, implemented by ``FeatDict``, act like + Python dictionaries. Feature identifiers may be strings or + instances of the ``Feature`` class. + - feature lists, implemented by ``FeatList``, act like Python + lists. Feature identifiers are integers. + + Feature structures may be indexed using either simple feature + identifiers or 'feature paths.' A feature path is a sequence + of feature identifiers that stand for a corresponding sequence of + indexing operations. In particular, ``fstruct[(f1,f2,...,fn)]`` is + equivalent to ``fstruct[f1][f2]...[fn]``. + + Feature structures may contain reentrant feature structures. A + "reentrant feature structure" is a single feature structure + object that can be accessed via multiple feature paths. Feature + structures may also be cyclic. A feature structure is "cyclic" + if there is any feature path from the feature structure to itself. + + Two feature structures are considered equal if they assign the + same values to all features, and have the same reentrancies. + + By default, feature structures are mutable. They may be made + immutable with the ``freeze()`` method. Once they have been + frozen, they may be hashed, and thus used as dictionary keys. + """ + + _frozen = False + """:ivar: A flag indicating whether this feature structure is + frozen or not. Once this flag is set, it should never be + un-set; and no further modification should be made to this + feature structure.""" + + ##//////////////////////////////////////////////////////////// + # { Constructor + ##//////////////////////////////////////////////////////////// + + def __new__(cls, features=None, **morefeatures): + """ + Construct and return a new feature structure. If this + constructor is called directly, then the returned feature + structure will be an instance of either the ``FeatDict`` class + or the ``FeatList`` class. + + :param features: The initial feature values for this feature + structure: + + - FeatStruct(string) -> FeatStructReader().read(string) + - FeatStruct(mapping) -> FeatDict(mapping) + - FeatStruct(sequence) -> FeatList(sequence) + - FeatStruct() -> FeatDict() + :param morefeatures: If ``features`` is a mapping or None, + then ``morefeatures`` provides additional features for the + ``FeatDict`` constructor. + """ + # If the FeatStruct constructor is called directly, then decide + # whether to create a FeatDict or a FeatList, based on the + # contents of the `features` argument. + if cls is FeatStruct: + if features is None: + return FeatDict.__new__(FeatDict, **morefeatures) + elif _is_mapping(features): + return FeatDict.__new__(FeatDict, features, **morefeatures) + elif morefeatures: + raise TypeError( + "Keyword arguments may only be specified " + "if features is None or is a mapping." + ) + if isinstance(features, str): + if FeatStructReader._START_FDICT_RE.match(features): + return FeatDict.__new__(FeatDict, features, **morefeatures) + else: + return FeatList.__new__(FeatList, features, **morefeatures) + elif _is_sequence(features): + return FeatList.__new__(FeatList, features) + else: + raise TypeError("Expected string or mapping or sequence") + + # Otherwise, construct the object as normal. + else: + return super().__new__(cls, features, **morefeatures) + + ##//////////////////////////////////////////////////////////// + # { Uniform Accessor Methods + ##//////////////////////////////////////////////////////////// + # These helper functions allow the methods defined by FeatStruct + # to treat all feature structures as mappings, even if they're + # really lists. (Lists are treated as mappings from ints to vals) + + def _keys(self): + """Return an iterable of the feature identifiers used by this + FeatStruct.""" + raise NotImplementedError() # Implemented by subclasses. + + def _values(self): + """Return an iterable of the feature values directly defined + by this FeatStruct.""" + raise NotImplementedError() # Implemented by subclasses. + + def _items(self): + """Return an iterable of (fid,fval) pairs, where fid is a + feature identifier and fval is the corresponding feature + value, for all features defined by this FeatStruct.""" + raise NotImplementedError() # Implemented by subclasses. + + ##//////////////////////////////////////////////////////////// + # { Equality & Hashing + ##//////////////////////////////////////////////////////////// + + def equal_values(self, other, check_reentrance=False): + """ + Return True if ``self`` and ``other`` assign the same value to + to every feature. In particular, return true if + ``self[p]==other[p]`` for every feature path *p* such + that ``self[p]`` or ``other[p]`` is a base value (i.e., + not a nested feature structure). + + :param check_reentrance: If True, then also return False if + there is any difference between the reentrances of ``self`` + and ``other``. + :note: the ``==`` is equivalent to ``equal_values()`` with + ``check_reentrance=True``. + """ + return self._equal(other, check_reentrance, set(), set(), set()) + + def __eq__(self, other): + """ + Return true if ``self`` and ``other`` are both feature structures, + assign the same values to all features, and contain the same + reentrances. I.e., return + ``self.equal_values(other, check_reentrance=True)``. + + :see: ``equal_values()`` + """ + return self._equal(other, True, set(), set(), set()) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, FeatStruct): + # raise_unorderable_types("<", self, other) + # Sometimes feature values can be pure strings, + # so we need to be able to compare with non-featstructs: + return self.__class__.__name__ < other.__class__.__name__ + else: + return len(self) < len(other) + + def __hash__(self): + """ + If this feature structure is frozen, return its hash value; + otherwise, raise ``TypeError``. + """ + if not self._frozen: + raise TypeError("FeatStructs must be frozen before they " "can be hashed.") + try: + return self._hash + except AttributeError: + self._hash = self._calculate_hashvalue(set()) + return self._hash + + def _equal( + self, other, check_reentrance, visited_self, visited_other, visited_pairs + ): + """ + Return True iff self and other have equal values. + + :param visited_self: A set containing the ids of all ``self`` + feature structures we've already visited. + :param visited_other: A set containing the ids of all ``other`` + feature structures we've already visited. + :param visited_pairs: A set containing ``(selfid, otherid)`` pairs + for all pairs of feature structures we've already visited. + """ + # If we're the same object, then we're equal. + if self is other: + return True + + # If we have different classes, we're definitely not equal. + if self.__class__ != other.__class__: + return False + + # If we define different features, we're definitely not equal. + # (Perform len test first because it's faster -- we should + # do profiling to see if this actually helps) + if len(self) != len(other): + return False + if set(self._keys()) != set(other._keys()): + return False + + # If we're checking reentrance, then any time we revisit a + # structure, make sure that it was paired with the same + # feature structure that it is now. Note: if check_reentrance, + # then visited_pairs will never contain two pairs whose first + # values are equal, or two pairs whose second values are equal. + if check_reentrance: + if id(self) in visited_self or id(other) in visited_other: + return (id(self), id(other)) in visited_pairs + + # If we're not checking reentrance, then we still need to deal + # with cycles. If we encounter the same (self, other) pair a + # second time, then we won't learn anything more by examining + # their children a second time, so just return true. + else: + if (id(self), id(other)) in visited_pairs: + return True + + # Keep track of which nodes we've visited. + visited_self.add(id(self)) + visited_other.add(id(other)) + visited_pairs.add((id(self), id(other))) + + # Now we have to check all values. If any of them don't match, + # then return false. + for fname, self_fval in self._items(): + other_fval = other[fname] + if isinstance(self_fval, FeatStruct): + if not self_fval._equal( + other_fval, + check_reentrance, + visited_self, + visited_other, + visited_pairs, + ): + return False + else: + if self_fval != other_fval: + return False + + # Everything matched up; return true. + return True + + def _calculate_hashvalue(self, visited): + """ + Return a hash value for this feature structure. + + :require: ``self`` must be frozen. + :param visited: A set containing the ids of all feature + structures we've already visited while hashing. + """ + if id(self) in visited: + return 1 + visited.add(id(self)) + + hashval = 5831 + for fname, fval in sorted(self._items()): + hashval *= 37 + hashval += hash(fname) + hashval *= 37 + if isinstance(fval, FeatStruct): + hashval += fval._calculate_hashvalue(visited) + else: + hashval += hash(fval) + # Convert to a 32 bit int. + hashval = int(hashval & 0x7FFFFFFF) + return hashval + + ##//////////////////////////////////////////////////////////// + # { Freezing + ##//////////////////////////////////////////////////////////// + + #: Error message used by mutating methods when called on a frozen + #: feature structure. + _FROZEN_ERROR = "Frozen FeatStructs may not be modified." + + def freeze(self): + """ + Make this feature structure, and any feature structures it + contains, immutable. Note: this method does not attempt to + 'freeze' any feature value that is not a ``FeatStruct``; it + is recommended that you use only immutable feature values. + """ + if self._frozen: + return + self._freeze(set()) + + def frozen(self): + """ + Return True if this feature structure is immutable. Feature + structures can be made immutable with the ``freeze()`` method. + Immutable feature structures may not be made mutable again, + but new mutable copies can be produced with the ``copy()`` method. + """ + return self._frozen + + def _freeze(self, visited): + """ + Make this feature structure, and any feature structure it + contains, immutable. + + :param visited: A set containing the ids of all feature + structures we've already visited while freezing. + """ + if id(self) in visited: + return + visited.add(id(self)) + self._frozen = True + for fname, fval in sorted(self._items()): + if isinstance(fval, FeatStruct): + fval._freeze(visited) + + ##//////////////////////////////////////////////////////////// + # { Copying + ##//////////////////////////////////////////////////////////// + + def copy(self, deep=True): + """ + Return a new copy of ``self``. The new copy will not be frozen. + + :param deep: If true, create a deep copy; if false, create + a shallow copy. + """ + if deep: + return copy.deepcopy(self) + else: + return self.__class__(self) + + # Subclasses should define __deepcopy__ to ensure that the new + # copy will not be frozen. + def __deepcopy__(self, memo): + raise NotImplementedError() # Implemented by subclasses. + + ##//////////////////////////////////////////////////////////// + # { Structural Information + ##//////////////////////////////////////////////////////////// + + def cyclic(self): + """ + Return True if this feature structure contains itself. + """ + return self._find_reentrances({})[id(self)] + + def walk(self): + """ + Return an iterator that generates this feature structure, and + each feature structure it contains. Each feature structure will + be generated exactly once. + """ + return self._walk(set()) + + def _walk(self, visited): + """ + Return an iterator that generates this feature structure, and + each feature structure it contains. + + :param visited: A set containing the ids of all feature + structures we've already visited while freezing. + """ + raise NotImplementedError() # Implemented by subclasses. + + def _walk(self, visited): + if id(self) in visited: + return + visited.add(id(self)) + yield self + for fval in self._values(): + if isinstance(fval, FeatStruct): + yield from fval._walk(visited) + + # Walk through the feature tree. The first time we see a feature + # value, map it to False (not reentrant). If we see a feature + # value more than once, then map it to True (reentrant). + def _find_reentrances(self, reentrances): + """ + Return a dictionary that maps from the ``id`` of each feature + structure contained in ``self`` (including ``self``) to a + boolean value, indicating whether it is reentrant or not. + """ + if id(self) in reentrances: + # We've seen it more than once. + reentrances[id(self)] = True + else: + # This is the first time we've seen it. + reentrances[id(self)] = False + + # Recurse to contained feature structures. + for fval in self._values(): + if isinstance(fval, FeatStruct): + fval._find_reentrances(reentrances) + + return reentrances + + ##//////////////////////////////////////////////////////////// + # { Variables & Bindings + ##//////////////////////////////////////////////////////////// + + def substitute_bindings(self, bindings): + """:see: ``nltk.featstruct.substitute_bindings()``""" + return substitute_bindings(self, bindings) + + def retract_bindings(self, bindings): + """:see: ``nltk.featstruct.retract_bindings()``""" + return retract_bindings(self, bindings) + + def variables(self): + """:see: ``nltk.featstruct.find_variables()``""" + return find_variables(self) + + def rename_variables(self, vars=None, used_vars=(), new_vars=None): + """:see: ``nltk.featstruct.rename_variables()``""" + return rename_variables(self, vars, used_vars, new_vars) + + def remove_variables(self): + """ + Return the feature structure that is obtained by deleting + any feature whose value is a ``Variable``. + + :rtype: FeatStruct + """ + return remove_variables(self) + + ##//////////////////////////////////////////////////////////// + # { Unification + ##//////////////////////////////////////////////////////////// + + def unify(self, other, bindings=None, trace=False, fail=None, rename_vars=True): + return unify(self, other, bindings, trace, fail, rename_vars) + + def subsumes(self, other): + """ + Return True if ``self`` subsumes ``other``. I.e., return true + If unifying ``self`` with ``other`` would result in a feature + structure equal to ``other``. + """ + return subsumes(self, other) + + ##//////////////////////////////////////////////////////////// + # { String Representations + ##//////////////////////////////////////////////////////////// + + def __repr__(self): + """ + Display a single-line representation of this feature structure, + suitable for embedding in other representations. + """ + return self._repr(self._find_reentrances({}), {}) + + def _repr(self, reentrances, reentrance_ids): + """ + Return a string representation of this feature structure. + + :param reentrances: A dictionary that maps from the ``id`` of + each feature value in self, indicating whether that value + is reentrant or not. + :param reentrance_ids: A dictionary mapping from each ``id`` + of a feature value to a unique identifier. This is modified + by ``repr``: the first time a reentrant feature value is + displayed, an identifier is added to ``reentrance_ids`` for it. + """ + raise NotImplementedError() + + +# Mutation: disable if frozen. +_FROZEN_ERROR = "Frozen FeatStructs may not be modified." +_FROZEN_NOTICE = "\n%sIf self is frozen, raise ValueError." + + +def _check_frozen(method, indent=""): + """ + Given a method function, return a new method function that first + checks if ``self._frozen`` is true; and if so, raises ``ValueError`` + with an appropriate message. Otherwise, call the method and return + its result. + """ + + def wrapped(self, *args, **kwargs): + if self._frozen: + raise ValueError(_FROZEN_ERROR) + else: + return method(self, *args, **kwargs) + + wrapped.__name__ = method.__name__ + wrapped.__doc__ = (method.__doc__ or "") + (_FROZEN_NOTICE % indent) + return wrapped + + +###################################################################### +# Feature Dictionary +###################################################################### + + +class FeatDict(FeatStruct, dict): + """ + A feature structure that acts like a Python dictionary. I.e., a + mapping from feature identifiers to feature values, where a feature + identifier can be a string or a ``Feature``; and where a feature value + can be either a basic value (such as a string or an integer), or a nested + feature structure. A feature identifiers for a ``FeatDict`` is + sometimes called a "feature name". + + Two feature dicts are considered equal if they assign the same + values to all features, and have the same reentrances. + + :see: ``FeatStruct`` for information about feature paths, reentrance, + cyclic feature structures, mutability, freezing, and hashing. + """ + + def __init__(self, features=None, **morefeatures): + """ + Create a new feature dictionary, with the specified features. + + :param features: The initial value for this feature + dictionary. If ``features`` is a ``FeatStruct``, then its + features are copied (shallow copy). If ``features`` is a + dict, then a feature is created for each item, mapping its + key to its value. If ``features`` is a string, then it is + processed using ``FeatStructReader``. If ``features`` is a list of + tuples ``(name, val)``, then a feature is created for each tuple. + :param morefeatures: Additional features for the new feature + dictionary. If a feature is listed under both ``features`` and + ``morefeatures``, then the value from ``morefeatures`` will be + used. + """ + if isinstance(features, str): + FeatStructReader().fromstring(features, self) + self.update(**morefeatures) + else: + # update() checks the types of features. + self.update(features, **morefeatures) + + # //////////////////////////////////////////////////////////// + # { Dict methods + # //////////////////////////////////////////////////////////// + _INDEX_ERROR = "Expected feature name or path. Got %r." + + def __getitem__(self, name_or_path): + """If the feature with the given name or path exists, return + its value; otherwise, raise ``KeyError``.""" + if isinstance(name_or_path, (str, Feature)): + return dict.__getitem__(self, name_or_path) + elif isinstance(name_or_path, tuple): + try: + val = self + for fid in name_or_path: + if not isinstance(val, FeatStruct): + raise KeyError # path contains base value + val = val[fid] + return val + except (KeyError, IndexError) as e: + raise KeyError(name_or_path) from e + else: + raise TypeError(self._INDEX_ERROR % name_or_path) + + def get(self, name_or_path, default=None): + """If the feature with the given name or path exists, return its + value; otherwise, return ``default``.""" + try: + return self[name_or_path] + except KeyError: + return default + + def __contains__(self, name_or_path): + """Return true if a feature with the given name or path exists.""" + try: + self[name_or_path] + return True + except KeyError: + return False + + def has_key(self, name_or_path): + """Return true if a feature with the given name or path exists.""" + return name_or_path in self + + def __delitem__(self, name_or_path): + """If the feature with the given name or path exists, delete + its value; otherwise, raise ``KeyError``.""" + if self._frozen: + raise ValueError(_FROZEN_ERROR) + if isinstance(name_or_path, (str, Feature)): + return dict.__delitem__(self, name_or_path) + elif isinstance(name_or_path, tuple): + if len(name_or_path) == 0: + raise ValueError("The path () can not be set") + else: + parent = self[name_or_path[:-1]] + if not isinstance(parent, FeatStruct): + raise KeyError(name_or_path) # path contains base value + del parent[name_or_path[-1]] + else: + raise TypeError(self._INDEX_ERROR % name_or_path) + + def __setitem__(self, name_or_path, value): + """Set the value for the feature with the given name or path + to ``value``. If ``name_or_path`` is an invalid path, raise + ``KeyError``.""" + if self._frozen: + raise ValueError(_FROZEN_ERROR) + if isinstance(name_or_path, (str, Feature)): + return dict.__setitem__(self, name_or_path, value) + elif isinstance(name_or_path, tuple): + if len(name_or_path) == 0: + raise ValueError("The path () can not be set") + else: + parent = self[name_or_path[:-1]] + if not isinstance(parent, FeatStruct): + raise KeyError(name_or_path) # path contains base value + parent[name_or_path[-1]] = value + else: + raise TypeError(self._INDEX_ERROR % name_or_path) + + clear = _check_frozen(dict.clear) + pop = _check_frozen(dict.pop) + popitem = _check_frozen(dict.popitem) + setdefault = _check_frozen(dict.setdefault) + + def update(self, features=None, **morefeatures): + if self._frozen: + raise ValueError(_FROZEN_ERROR) + if features is None: + items = () + elif hasattr(features, "items") and callable(features.items): + items = features.items() + elif hasattr(features, "__iter__"): + items = features + else: + raise ValueError("Expected mapping or list of tuples") + + for key, val in items: + if not isinstance(key, (str, Feature)): + raise TypeError("Feature names must be strings") + self[key] = val + for key, val in morefeatures.items(): + if not isinstance(key, (str, Feature)): + raise TypeError("Feature names must be strings") + self[key] = val + + ##//////////////////////////////////////////////////////////// + # { Copying + ##//////////////////////////////////////////////////////////// + + def __deepcopy__(self, memo): + memo[id(self)] = selfcopy = self.__class__() + for key, val in self._items(): + selfcopy[copy.deepcopy(key, memo)] = copy.deepcopy(val, memo) + return selfcopy + + ##//////////////////////////////////////////////////////////// + # { Uniform Accessor Methods + ##//////////////////////////////////////////////////////////// + + def _keys(self): + return self.keys() + + def _values(self): + return self.values() + + def _items(self): + return self.items() + + ##//////////////////////////////////////////////////////////// + # { String Representations + ##//////////////////////////////////////////////////////////// + + def __str__(self): + """ + Display a multi-line representation of this feature dictionary + as an FVM (feature value matrix). + """ + return "\n".join(self._str(self._find_reentrances({}), {})) + + def _repr(self, reentrances, reentrance_ids): + segments = [] + prefix = "" + suffix = "" + + # If this is the first time we've seen a reentrant structure, + # then assign it a unique identifier. + if reentrances[id(self)]: + assert id(self) not in reentrance_ids + reentrance_ids[id(self)] = repr(len(reentrance_ids) + 1) + + # sorting note: keys are unique strings, so we'll never fall + # through to comparing values. + for fname, fval in sorted(self.items()): + display = getattr(fname, "display", None) + if id(fval) in reentrance_ids: + segments.append(f"{fname}->({reentrance_ids[id(fval)]})") + elif ( + display == "prefix" and not prefix and isinstance(fval, (Variable, str)) + ): + prefix = "%s" % fval + elif display == "slash" and not suffix: + if isinstance(fval, Variable): + suffix = "/%s" % fval.name + else: + suffix = "/%s" % repr(fval) + elif isinstance(fval, Variable): + segments.append(f"{fname}={fval.name}") + elif fval is True: + segments.append("+%s" % fname) + elif fval is False: + segments.append("-%s" % fname) + elif isinstance(fval, Expression): + segments.append(f"{fname}=<{fval}>") + elif not isinstance(fval, FeatStruct): + segments.append(f"{fname}={repr(fval)}") + else: + fval_repr = fval._repr(reentrances, reentrance_ids) + segments.append(f"{fname}={fval_repr}") + # If it's reentrant, then add on an identifier tag. + if reentrances[id(self)]: + prefix = f"({reentrance_ids[id(self)]}){prefix}" + return "{}[{}]{}".format(prefix, ", ".join(segments), suffix) + + def _str(self, reentrances, reentrance_ids): + """ + :return: A list of lines composing a string representation of + this feature dictionary. + :param reentrances: A dictionary that maps from the ``id`` of + each feature value in self, indicating whether that value + is reentrant or not. + :param reentrance_ids: A dictionary mapping from each ``id`` + of a feature value to a unique identifier. This is modified + by ``repr``: the first time a reentrant feature value is + displayed, an identifier is added to ``reentrance_ids`` for + it. + """ + # If this is the first time we've seen a reentrant structure, + # then tack on an id string. + if reentrances[id(self)]: + assert id(self) not in reentrance_ids + reentrance_ids[id(self)] = repr(len(reentrance_ids) + 1) + + # Special case: empty feature dict. + if len(self) == 0: + if reentrances[id(self)]: + return ["(%s) []" % reentrance_ids[id(self)]] + else: + return ["[]"] + + # What's the longest feature name? Use this to align names. + maxfnamelen = max(len("%s" % k) for k in self.keys()) + + lines = [] + # sorting note: keys are unique strings, so we'll never fall + # through to comparing values. + for fname, fval in sorted(self.items()): + fname = ("%s" % fname).ljust(maxfnamelen) + if isinstance(fval, Variable): + lines.append(f"{fname} = {fval.name}") + + elif isinstance(fval, Expression): + lines.append(f"{fname} = <{fval}>") + + elif isinstance(fval, FeatList): + fval_repr = fval._repr(reentrances, reentrance_ids) + lines.append(f"{fname} = {repr(fval_repr)}") + + elif not isinstance(fval, FeatDict): + # It's not a nested feature structure -- just print it. + lines.append(f"{fname} = {repr(fval)}") + + elif id(fval) in reentrance_ids: + # It's a feature structure we've seen before -- print + # the reentrance id. + lines.append(f"{fname} -> ({reentrance_ids[id(fval)]})") + + else: + # It's a new feature structure. Separate it from + # other values by a blank line. + if lines and lines[-1] != "": + lines.append("") + + # Recursively print the feature's value (fval). + fval_lines = fval._str(reentrances, reentrance_ids) + + # Indent each line to make room for fname. + fval_lines = [(" " * (maxfnamelen + 3)) + l for l in fval_lines] + + # Pick which line we'll display fname on, & splice it in. + nameline = (len(fval_lines) - 1) // 2 + fval_lines[nameline] = ( + fname + " =" + fval_lines[nameline][maxfnamelen + 2 :] + ) + + # Add the feature structure to the output. + lines += fval_lines + + # Separate FeatStructs by a blank line. + lines.append("") + + # Get rid of any excess blank lines. + if lines[-1] == "": + lines.pop() + + # Add brackets around everything. + maxlen = max(len(line) for line in lines) + lines = ["[ {}{} ]".format(line, " " * (maxlen - len(line))) for line in lines] + + # If it's reentrant, then add on an identifier tag. + if reentrances[id(self)]: + idstr = "(%s) " % reentrance_ids[id(self)] + lines = [(" " * len(idstr)) + l for l in lines] + idline = (len(lines) - 1) // 2 + lines[idline] = idstr + lines[idline][len(idstr) :] + + return lines + + +###################################################################### +# Feature List +###################################################################### + + +class FeatList(FeatStruct, list): + """ + A list of feature values, where each feature value is either a + basic value (such as a string or an integer), or a nested feature + structure. + + Feature lists may contain reentrant feature values. A "reentrant + feature value" is a single feature value that can be accessed via + multiple feature paths. Feature lists may also be cyclic. + + Two feature lists are considered equal if they assign the same + values to all features, and have the same reentrances. + + :see: ``FeatStruct`` for information about feature paths, reentrance, + cyclic feature structures, mutability, freezing, and hashing. + """ + + def __init__(self, features=()): + """ + Create a new feature list, with the specified features. + + :param features: The initial list of features for this feature + list. If ``features`` is a string, then it is paresd using + ``FeatStructReader``. Otherwise, it should be a sequence + of basic values and nested feature structures. + """ + if isinstance(features, str): + FeatStructReader().fromstring(features, self) + else: + list.__init__(self, features) + + # //////////////////////////////////////////////////////////// + # { List methods + # //////////////////////////////////////////////////////////// + _INDEX_ERROR = "Expected int or feature path. Got %r." + + def __getitem__(self, name_or_path): + if isinstance(name_or_path, int): + return list.__getitem__(self, name_or_path) + elif isinstance(name_or_path, tuple): + try: + val = self + for fid in name_or_path: + if not isinstance(val, FeatStruct): + raise KeyError # path contains base value + val = val[fid] + return val + except (KeyError, IndexError) as e: + raise KeyError(name_or_path) from e + else: + raise TypeError(self._INDEX_ERROR % name_or_path) + + def __delitem__(self, name_or_path): + """If the feature with the given name or path exists, delete + its value; otherwise, raise ``KeyError``.""" + if self._frozen: + raise ValueError(_FROZEN_ERROR) + if isinstance(name_or_path, (int, slice)): + return list.__delitem__(self, name_or_path) + elif isinstance(name_or_path, tuple): + if len(name_or_path) == 0: + raise ValueError("The path () can not be set") + else: + parent = self[name_or_path[:-1]] + if not isinstance(parent, FeatStruct): + raise KeyError(name_or_path) # path contains base value + del parent[name_or_path[-1]] + else: + raise TypeError(self._INDEX_ERROR % name_or_path) + + def __setitem__(self, name_or_path, value): + """Set the value for the feature with the given name or path + to ``value``. If ``name_or_path`` is an invalid path, raise + ``KeyError``.""" + if self._frozen: + raise ValueError(_FROZEN_ERROR) + if isinstance(name_or_path, (int, slice)): + return list.__setitem__(self, name_or_path, value) + elif isinstance(name_or_path, tuple): + if len(name_or_path) == 0: + raise ValueError("The path () can not be set") + else: + parent = self[name_or_path[:-1]] + if not isinstance(parent, FeatStruct): + raise KeyError(name_or_path) # path contains base value + parent[name_or_path[-1]] = value + else: + raise TypeError(self._INDEX_ERROR % name_or_path) + + # __delslice__ = _check_frozen(list.__delslice__, ' ') + # __setslice__ = _check_frozen(list.__setslice__, ' ') + __iadd__ = _check_frozen(list.__iadd__) + __imul__ = _check_frozen(list.__imul__) + append = _check_frozen(list.append) + extend = _check_frozen(list.extend) + insert = _check_frozen(list.insert) + pop = _check_frozen(list.pop) + remove = _check_frozen(list.remove) + reverse = _check_frozen(list.reverse) + sort = _check_frozen(list.sort) + + ##//////////////////////////////////////////////////////////// + # { Copying + ##//////////////////////////////////////////////////////////// + + def __deepcopy__(self, memo): + memo[id(self)] = selfcopy = self.__class__() + selfcopy.extend(copy.deepcopy(fval, memo) for fval in self) + return selfcopy + + ##//////////////////////////////////////////////////////////// + # { Uniform Accessor Methods + ##//////////////////////////////////////////////////////////// + + def _keys(self): + return list(range(len(self))) + + def _values(self): + return self + + def _items(self): + return enumerate(self) + + ##//////////////////////////////////////////////////////////// + # { String Representations + ##//////////////////////////////////////////////////////////// + + # Special handling for: reentrances, variables, expressions. + def _repr(self, reentrances, reentrance_ids): + # If this is the first time we've seen a reentrant structure, + # then assign it a unique identifier. + if reentrances[id(self)]: + assert id(self) not in reentrance_ids + reentrance_ids[id(self)] = repr(len(reentrance_ids) + 1) + prefix = "(%s)" % reentrance_ids[id(self)] + else: + prefix = "" + + segments = [] + for fval in self: + if id(fval) in reentrance_ids: + segments.append("->(%s)" % reentrance_ids[id(fval)]) + elif isinstance(fval, Variable): + segments.append(fval.name) + elif isinstance(fval, Expression): + segments.append("%s" % fval) + elif isinstance(fval, FeatStruct): + segments.append(fval._repr(reentrances, reentrance_ids)) + else: + segments.append("%s" % repr(fval)) + + return "{}[{}]".format(prefix, ", ".join(segments)) + + +###################################################################### +# Variables & Bindings +###################################################################### + + +def substitute_bindings(fstruct, bindings, fs_class="default"): + """ + Return the feature structure that is obtained by replacing each + variable bound by ``bindings`` with its binding. If a variable is + aliased to a bound variable, then it will be replaced by that + variable's value. If a variable is aliased to an unbound + variable, then it will be replaced by that variable. + + :type bindings: dict(Variable -> any) + :param bindings: A dictionary mapping from variables to values. + """ + if fs_class == "default": + fs_class = _default_fs_class(fstruct) + fstruct = copy.deepcopy(fstruct) + _substitute_bindings(fstruct, bindings, fs_class, set()) + return fstruct + + +def _substitute_bindings(fstruct, bindings, fs_class, visited): + # Visit each node only once: + if id(fstruct) in visited: + return + visited.add(id(fstruct)) + + if _is_mapping(fstruct): + items = fstruct.items() + elif _is_sequence(fstruct): + items = enumerate(fstruct) + else: + raise ValueError("Expected mapping or sequence") + for fname, fval in items: + while isinstance(fval, Variable) and fval in bindings: + fval = fstruct[fname] = bindings[fval] + if isinstance(fval, fs_class): + _substitute_bindings(fval, bindings, fs_class, visited) + elif isinstance(fval, SubstituteBindingsI): + fstruct[fname] = fval.substitute_bindings(bindings) + + +def retract_bindings(fstruct, bindings, fs_class="default"): + """ + Return the feature structure that is obtained by replacing each + feature structure value that is bound by ``bindings`` with the + variable that binds it. A feature structure value must be + identical to a bound value (i.e., have equal id) to be replaced. + + ``bindings`` is modified to point to this new feature structure, + rather than the original feature structure. Feature structure + values in ``bindings`` may be modified if they are contained in + ``fstruct``. + """ + if fs_class == "default": + fs_class = _default_fs_class(fstruct) + (fstruct, new_bindings) = copy.deepcopy((fstruct, bindings)) + bindings.update(new_bindings) + inv_bindings = {id(val): var for (var, val) in bindings.items()} + _retract_bindings(fstruct, inv_bindings, fs_class, set()) + return fstruct + + +def _retract_bindings(fstruct, inv_bindings, fs_class, visited): + # Visit each node only once: + if id(fstruct) in visited: + return + visited.add(id(fstruct)) + + if _is_mapping(fstruct): + items = fstruct.items() + elif _is_sequence(fstruct): + items = enumerate(fstruct) + else: + raise ValueError("Expected mapping or sequence") + for fname, fval in items: + if isinstance(fval, fs_class): + if id(fval) in inv_bindings: + fstruct[fname] = inv_bindings[id(fval)] + _retract_bindings(fval, inv_bindings, fs_class, visited) + + +def find_variables(fstruct, fs_class="default"): + """ + :return: The set of variables used by this feature structure. + :rtype: set(Variable) + """ + if fs_class == "default": + fs_class = _default_fs_class(fstruct) + return _variables(fstruct, set(), fs_class, set()) + + +def _variables(fstruct, vars, fs_class, visited): + # Visit each node only once: + if id(fstruct) in visited: + return + visited.add(id(fstruct)) + if _is_mapping(fstruct): + items = fstruct.items() + elif _is_sequence(fstruct): + items = enumerate(fstruct) + else: + raise ValueError("Expected mapping or sequence") + for fname, fval in items: + if isinstance(fval, Variable): + vars.add(fval) + elif isinstance(fval, fs_class): + _variables(fval, vars, fs_class, visited) + elif isinstance(fval, SubstituteBindingsI): + vars.update(fval.variables()) + return vars + + +def rename_variables( + fstruct, vars=None, used_vars=(), new_vars=None, fs_class="default" +): + """ + Return the feature structure that is obtained by replacing + any of this feature structure's variables that are in ``vars`` + with new variables. The names for these new variables will be + names that are not used by any variable in ``vars``, or in + ``used_vars``, or in this feature structure. + + :type vars: set + :param vars: The set of variables that should be renamed. + If not specified, ``find_variables(fstruct)`` is used; i.e., all + variables will be given new names. + :type used_vars: set + :param used_vars: A set of variables whose names should not be + used by the new variables. + :type new_vars: dict(Variable -> Variable) + :param new_vars: A dictionary that is used to hold the mapping + from old variables to new variables. For each variable *v* + in this feature structure: + + - If ``new_vars`` maps *v* to *v'*, then *v* will be + replaced by *v'*. + - If ``new_vars`` does not contain *v*, but ``vars`` + does contain *v*, then a new entry will be added to + ``new_vars``, mapping *v* to the new variable that is used + to replace it. + + To consistently rename the variables in a set of feature + structures, simply apply rename_variables to each one, using + the same dictionary: + + >>> from nltk.featstruct import FeatStruct + >>> fstruct1 = FeatStruct('[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]') + >>> fstruct2 = FeatStruct('[subj=[agr=[number=?z,gender=?y]], obj=[agr=[number=?z,gender=?y]]]') + >>> new_vars = {} # Maps old vars to alpha-renamed vars + >>> fstruct1.rename_variables(new_vars=new_vars) + [obj=[agr=[gender=?y2]], subj=[agr=[gender=?y2]]] + >>> fstruct2.rename_variables(new_vars=new_vars) + [obj=[agr=[gender=?y2, number=?z2]], subj=[agr=[gender=?y2, number=?z2]]] + + If new_vars is not specified, then an empty dictionary is used. + """ + if fs_class == "default": + fs_class = _default_fs_class(fstruct) + + # Default values: + if new_vars is None: + new_vars = {} + if vars is None: + vars = find_variables(fstruct, fs_class) + else: + vars = set(vars) + + # Add our own variables to used_vars. + used_vars = find_variables(fstruct, fs_class).union(used_vars) + + # Copy ourselves, and rename variables in the copy. + return _rename_variables( + copy.deepcopy(fstruct), vars, used_vars, new_vars, fs_class, set() + ) + + +def _rename_variables(fstruct, vars, used_vars, new_vars, fs_class, visited): + if id(fstruct) in visited: + return + visited.add(id(fstruct)) + if _is_mapping(fstruct): + items = fstruct.items() + elif _is_sequence(fstruct): + items = enumerate(fstruct) + else: + raise ValueError("Expected mapping or sequence") + for fname, fval in items: + if isinstance(fval, Variable): + # If it's in new_vars, then rebind it. + if fval in new_vars: + fstruct[fname] = new_vars[fval] + # If it's in vars, pick a new name for it. + elif fval in vars: + new_vars[fval] = _rename_variable(fval, used_vars) + fstruct[fname] = new_vars[fval] + used_vars.add(new_vars[fval]) + elif isinstance(fval, fs_class): + _rename_variables(fval, vars, used_vars, new_vars, fs_class, visited) + elif isinstance(fval, SubstituteBindingsI): + # Pick new names for any variables in `vars` + for var in fval.variables(): + if var in vars and var not in new_vars: + new_vars[var] = _rename_variable(var, used_vars) + used_vars.add(new_vars[var]) + # Replace all variables in `new_vars`. + fstruct[fname] = fval.substitute_bindings(new_vars) + return fstruct + + +def _rename_variable(var, used_vars): + # Strip a trailing run of digits from the variable name. The ``(?>> from nltk.featstruct import FeatStruct + >>> FeatStruct('[a=?x]').unify(FeatStruct('[b=?x]')) + [a=?x, b=?x2] + + :type bindings: dict(Variable -> any) + :param bindings: A set of variable bindings to be used and + updated during unification. + :type trace: bool + :param trace: If true, generate trace output. + :type rename_vars: bool + :param rename_vars: If True, then rename any variables in + ``fstruct2`` that are also used in ``fstruct1``, in order to + avoid collisions on variable names. + """ + # Decide which class(es) will be treated as feature structures, + # for the purposes of unification. + if fs_class == "default": + fs_class = _default_fs_class(fstruct1) + if _default_fs_class(fstruct2) != fs_class: + raise ValueError( + "Mixing FeatStruct objects with Python " + "dicts and lists is not supported." + ) + assert isinstance(fstruct1, fs_class) + assert isinstance(fstruct2, fs_class) + + # If bindings are unspecified, use an empty set of bindings. + user_bindings = bindings is not None + if bindings is None: + bindings = {} + + # Make copies of fstruct1 and fstruct2 (since the unification + # algorithm is destructive). Do it all at once, to preserve + # reentrance links between fstruct1 and fstruct2. Copy bindings + # as well, in case there are any bound vars that contain parts + # of fstruct1 or fstruct2. + (fstruct1copy, fstruct2copy, bindings_copy) = copy.deepcopy( + (fstruct1, fstruct2, bindings) + ) + + # Copy the bindings back to the original bindings dict. + bindings.update(bindings_copy) + + if rename_vars: + vars1 = find_variables(fstruct1copy, fs_class) + vars2 = find_variables(fstruct2copy, fs_class) + _rename_variables(fstruct2copy, vars1, vars2, {}, fs_class, set()) + + # Do the actual unification. If it fails, return None. + forward = {} + if trace: + _trace_unify_start((), fstruct1copy, fstruct2copy) + try: + result = _destructively_unify( + fstruct1copy, fstruct2copy, bindings, forward, trace, fail, fs_class, () + ) + except _UnificationFailureError: + return None + + # _destructively_unify might return UnificationFailure, e.g. if we + # tried to unify a mapping with a sequence. + if result is UnificationFailure: + if fail is None: + return None + else: + return fail(fstruct1copy, fstruct2copy, ()) + + # Replace any feature structure that has a forward pointer + # with the target of its forward pointer. + result = _apply_forwards(result, forward, fs_class, set()) + if user_bindings: + _apply_forwards_to_bindings(forward, bindings) + + # Replace bound vars with values. + _resolve_aliases(bindings) + _substitute_bindings(result, bindings, fs_class, set()) + + # Return the result. + if trace: + _trace_unify_succeed((), result) + if trace: + _trace_bindings((), bindings) + return result + + +class _UnificationFailureError(Exception): + """An exception that is used by ``_destructively_unify`` to abort + unification when a failure is encountered.""" + + +def _destructively_unify( + fstruct1, fstruct2, bindings, forward, trace, fail, fs_class, path +): + """ + Attempt to unify ``fstruct1`` and ``fstruct2`` by modifying them + in-place. If the unification succeeds, then ``fstruct1`` will + contain the unified value, the value of ``fstruct2`` is undefined, + and forward[id(fstruct2)] is set to fstruct1. If the unification + fails, then a _UnificationFailureError is raised, and the + values of ``fstruct1`` and ``fstruct2`` are undefined. + + :param bindings: A dictionary mapping variables to values. + :param forward: A dictionary mapping feature structures ids + to replacement structures. When two feature structures + are merged, a mapping from one to the other will be added + to the forward dictionary; and changes will be made only + to the target of the forward dictionary. + ``_destructively_unify`` will always 'follow' any links + in the forward dictionary for fstruct1 and fstruct2 before + actually unifying them. + :param trace: If true, generate trace output + :param path: The feature path that led us to this unification + step. Used for trace output. + """ + # If fstruct1 is already identical to fstruct2, we're done. + # Note: this, together with the forward pointers, ensures + # that unification will terminate even for cyclic structures. + if fstruct1 is fstruct2: + if trace: + _trace_unify_identity(path, fstruct1) + return fstruct1 + + # Set fstruct2's forward pointer to point to fstruct1; this makes + # fstruct1 the canonical copy for fstruct2. Note that we need to + # do this before we recurse into any child structures, in case + # they're cyclic. + forward[id(fstruct2)] = fstruct1 + + # Unifying two mappings: + if _is_mapping(fstruct1) and _is_mapping(fstruct2): + for fname in fstruct1: + if getattr(fname, "default", None) is not None: + fstruct2.setdefault(fname, fname.default) + for fname in fstruct2: + if getattr(fname, "default", None) is not None: + fstruct1.setdefault(fname, fname.default) + + # Unify any values that are defined in both fstruct1 and + # fstruct2. Copy any values that are defined in fstruct2 but + # not in fstruct1 to fstruct1. Note: sorting fstruct2's + # features isn't actually necessary; but we do it to give + # deterministic behavior, e.g. for tracing. + for fname, fval2 in sorted(fstruct2.items()): + if fname in fstruct1: + fstruct1[fname] = _unify_feature_values( + fname, + fstruct1[fname], + fval2, + bindings, + forward, + trace, + fail, + fs_class, + path + (fname,), + ) + else: + fstruct1[fname] = fval2 + + return fstruct1 # Contains the unified value. + + # Unifying two sequences: + elif _is_sequence(fstruct1) and _is_sequence(fstruct2): + # If the lengths don't match, fail. + if len(fstruct1) != len(fstruct2): + return UnificationFailure + + # Unify corresponding values in fstruct1 and fstruct2. + for findex in range(len(fstruct1)): + fstruct1[findex] = _unify_feature_values( + findex, + fstruct1[findex], + fstruct2[findex], + bindings, + forward, + trace, + fail, + fs_class, + path + (findex,), + ) + + return fstruct1 # Contains the unified value. + + # Unifying sequence & mapping: fail. The failure function + # doesn't get a chance to recover in this case. + elif (_is_sequence(fstruct1) or _is_mapping(fstruct1)) and ( + _is_sequence(fstruct2) or _is_mapping(fstruct2) + ): + return UnificationFailure + + # Unifying anything else: not allowed! + raise TypeError("Expected mappings or sequences") + + +def _unify_feature_values( + fname, fval1, fval2, bindings, forward, trace, fail, fs_class, fpath +): + """ + Attempt to unify ``fval1`` and and ``fval2``, and return the + resulting unified value. The method of unification will depend on + the types of ``fval1`` and ``fval2``: + + 1. If they're both feature structures, then destructively + unify them (see ``_destructively_unify()``. + 2. If they're both unbound variables, then alias one variable + to the other (by setting bindings[v2]=v1). + 3. If one is an unbound variable, and the other is a value, + then bind the unbound variable to the value. + 4. If one is a feature structure, and the other is a base value, + then fail. + 5. If they're both base values, then unify them. By default, + this will succeed if they are equal, and fail otherwise. + """ + if trace: + _trace_unify_start(fpath, fval1, fval2) + + # Look up the "canonical" copy of fval1 and fval2 + while id(fval1) in forward: + fval1 = forward[id(fval1)] + while id(fval2) in forward: + fval2 = forward[id(fval2)] + + # If fval1 or fval2 is a bound variable, then + # replace it by the variable's bound value. This + # includes aliased variables, which are encoded as + # variables bound to other variables. + fvar1 = fvar2 = None + while isinstance(fval1, Variable) and fval1 in bindings: + fvar1 = fval1 + fval1 = bindings[fval1] + while isinstance(fval2, Variable) and fval2 in bindings: + fvar2 = fval2 + fval2 = bindings[fval2] + + # Case 1: Two feature structures (recursive case) + if isinstance(fval1, fs_class) and isinstance(fval2, fs_class): + result = _destructively_unify( + fval1, fval2, bindings, forward, trace, fail, fs_class, fpath + ) + + # Case 2: Two unbound variables (create alias) + elif isinstance(fval1, Variable) and isinstance(fval2, Variable): + if fval1 != fval2: + bindings[fval2] = fval1 + result = fval1 + + # Case 3: An unbound variable and a value (bind) + elif isinstance(fval1, Variable): + bindings[fval1] = fval2 + result = fval1 + elif isinstance(fval2, Variable): + bindings[fval2] = fval1 + result = fval2 + + # Case 4: A feature structure & a base value (fail) + elif isinstance(fval1, fs_class) or isinstance(fval2, fs_class): + result = UnificationFailure + + # Case 5: Two base values + else: + # Case 5a: Feature defines a custom unification method for base values + if isinstance(fname, Feature): + result = fname.unify_base_values(fval1, fval2, bindings) + # Case 5b: Feature value defines custom unification method + elif isinstance(fval1, CustomFeatureValue): + result = fval1.unify(fval2) + # Sanity check: unify value should be symmetric + if isinstance(fval2, CustomFeatureValue) and result != fval2.unify(fval1): + raise AssertionError( + "CustomFeatureValue objects %r and %r disagree " + "about unification value: %r vs. %r" + % (fval1, fval2, result, fval2.unify(fval1)) + ) + elif isinstance(fval2, CustomFeatureValue): + result = fval2.unify(fval1) + # Case 5c: Simple values -- check if they're equal. + else: + if fval1 == fval2: + result = fval1 + else: + result = UnificationFailure + + # If either value was a bound variable, then update the + # bindings. (This is really only necessary if fname is a + # Feature or if either value is a CustomFeatureValue.) + if result is not UnificationFailure: + if fvar1 is not None: + bindings[fvar1] = result + result = fvar1 + if fvar2 is not None and fvar2 != fvar1: + bindings[fvar2] = result + result = fvar2 + + # If we unification failed, call the failure function; it + # might decide to continue anyway. + if result is UnificationFailure: + if fail is not None: + result = fail(fval1, fval2, fpath) + if trace: + _trace_unify_fail(fpath[:-1], result) + if result is UnificationFailure: + raise _UnificationFailureError + + # Normalize the result. + if isinstance(result, fs_class): + result = _apply_forwards(result, forward, fs_class, set()) + + if trace: + _trace_unify_succeed(fpath, result) + if trace and isinstance(result, fs_class): + _trace_bindings(fpath, bindings) + + return result + + +def _apply_forwards_to_bindings(forward, bindings): + """ + Replace any feature structure that has a forward pointer with + the target of its forward pointer (to preserve reentrancy). + """ + for var, value in bindings.items(): + while id(value) in forward: + value = forward[id(value)] + bindings[var] = value + + +def _apply_forwards(fstruct, forward, fs_class, visited): + """ + Replace any feature structure that has a forward pointer with + the target of its forward pointer (to preserve reentrancy). + """ + # Follow our own forwards pointers (if any) + while id(fstruct) in forward: + fstruct = forward[id(fstruct)] + + # Visit each node only once: + if id(fstruct) in visited: + return + visited.add(id(fstruct)) + + if _is_mapping(fstruct): + items = fstruct.items() + elif _is_sequence(fstruct): + items = enumerate(fstruct) + else: + raise ValueError("Expected mapping or sequence") + for fname, fval in items: + if isinstance(fval, fs_class): + # Replace w/ forwarded value. + while id(fval) in forward: + fval = forward[id(fval)] + fstruct[fname] = fval + # Recurse to child. + _apply_forwards(fval, forward, fs_class, visited) + + return fstruct + + +def _resolve_aliases(bindings): + """ + Replace any bound aliased vars with their binding; and replace + any unbound aliased vars with their representative var. + """ + for var, value in bindings.items(): + while isinstance(value, Variable) and value in bindings: + value = bindings[var] = bindings[value] + + +def _trace_unify_start(path, fval1, fval2): + if path == (): + print("\nUnification trace:") + else: + fullname = ".".join("%s" % n for n in path) + print(" " + "| " * (len(path) - 1) + "|") + print(" " + "| " * (len(path) - 1) + "| Unify feature: %s" % fullname) + print(" " + "| " * len(path) + " / " + _trace_valrepr(fval1)) + print(" " + "| " * len(path) + "|\\ " + _trace_valrepr(fval2)) + + +def _trace_unify_identity(path, fval1): + print(" " + "| " * len(path) + "|") + print(" " + "| " * len(path) + "| (identical objects)") + print(" " + "| " * len(path) + "|") + print(" " + "| " * len(path) + "+-->" + repr(fval1)) + + +def _trace_unify_fail(path, result): + if result is UnificationFailure: + resume = "" + else: + resume = " (nonfatal)" + print(" " + "| " * len(path) + "| |") + print(" " + "X " * len(path) + "X X <-- FAIL" + resume) + + +def _trace_unify_succeed(path, fval1): + # Print the result. + print(" " + "| " * len(path) + "|") + print(" " + "| " * len(path) + "+-->" + repr(fval1)) + + +def _trace_bindings(path, bindings): + # Print the bindings (if any). + if len(bindings) > 0: + binditems = sorted(bindings.items(), key=lambda v: v[0].name) + bindstr = "{%s}" % ", ".join( + f"{var}: {_trace_valrepr(val)}" for (var, val) in binditems + ) + print(" " + "| " * len(path) + " Bindings: " + bindstr) + + +def _trace_valrepr(val): + if isinstance(val, Variable): + return "%s" % val + else: + return "%s" % repr(val) + + +def subsumes(fstruct1, fstruct2): + """ + Return True if ``fstruct1`` subsumes ``fstruct2``. I.e., return + true if unifying ``fstruct1`` with ``fstruct2`` would result in a + feature structure equal to ``fstruct2.`` + + :rtype: bool + """ + return fstruct2 == unify(fstruct1, fstruct2) + + +def conflicts(fstruct1, fstruct2, trace=0): + """ + Return a list of the feature paths of all features which are + assigned incompatible values by ``fstruct1`` and ``fstruct2``. + + :rtype: list(tuple) + """ + conflict_list = [] + + def add_conflict(fval1, fval2, path): + conflict_list.append(path) + return fval1 + + unify(fstruct1, fstruct2, fail=add_conflict, trace=trace) + return conflict_list + + +###################################################################### +# Helper Functions +###################################################################### + + +def _is_mapping(v): + return hasattr(v, "__contains__") and hasattr(v, "keys") + + +def _is_sequence(v): + return hasattr(v, "__iter__") and hasattr(v, "__len__") and not isinstance(v, str) + + +def _default_fs_class(obj): + if isinstance(obj, FeatStruct): + return FeatStruct + if isinstance(obj, (dict, list)): + return (dict, list) + else: + raise ValueError( + "To unify objects of type %s, you must specify " + "fs_class explicitly." % obj.__class__.__name__ + ) + + +###################################################################### +# FeatureValueSet & FeatureValueTuple +###################################################################### + + +class SubstituteBindingsSequence(SubstituteBindingsI): + """ + A mixin class for sequence classes that distributes variables() and + substitute_bindings() over the object's elements. + """ + + def variables(self): + return [elt for elt in self if isinstance(elt, Variable)] + sum( + ( + list(elt.variables()) + for elt in self + if isinstance(elt, SubstituteBindingsI) + ), + [], + ) + + def substitute_bindings(self, bindings): + return self.__class__([self.subst(v, bindings) for v in self]) + + def subst(self, v, bindings): + if isinstance(v, SubstituteBindingsI): + return v.substitute_bindings(bindings) + else: + return bindings.get(v, v) + + +class FeatureValueTuple(SubstituteBindingsSequence, tuple): + """ + A base feature value that is a tuple of other base feature values. + FeatureValueTuple implements ``SubstituteBindingsI``, so it any + variable substitutions will be propagated to the elements + contained by the set. A ``FeatureValueTuple`` is immutable. + """ + + def __repr__(self): # [xx] really use %s here? + if len(self) == 0: + return "()" + return "(%s)" % ", ".join(f"{b}" for b in self) + + +class FeatureValueSet(SubstituteBindingsSequence, frozenset): + """ + A base feature value that is a set of other base feature values. + FeatureValueSet implements ``SubstituteBindingsI``, so it any + variable substitutions will be propagated to the elements + contained by the set. A ``FeatureValueSet`` is immutable. + """ + + def __repr__(self): # [xx] really use %s here? + if len(self) == 0: + return "{/}" # distinguish from dict. + # n.b., we sort the string reprs of our elements, to ensure + # that our own repr is deterministic. + return "{%s}" % ", ".join(sorted(f"{b}" for b in self)) + + __str__ = __repr__ + + +class FeatureValueUnion(SubstituteBindingsSequence, frozenset): + """ + A base feature value that represents the union of two or more + ``FeatureValueSet`` or ``Variable``. + """ + + def __new__(cls, values): + # If values contains FeatureValueUnions, then collapse them. + values = _flatten(values, FeatureValueUnion) + + # If the resulting list contains no variables, then + # use a simple FeatureValueSet instead. + if sum(isinstance(v, Variable) for v in values) == 0: + values = _flatten(values, FeatureValueSet) + return FeatureValueSet(values) + + # If we contain a single variable, return that variable. + if len(values) == 1: + return list(values)[0] + + # Otherwise, build the FeatureValueUnion. + return frozenset.__new__(cls, values) + + def __repr__(self): + # n.b., we sort the string reprs of our elements, to ensure + # that our own repr is deterministic. also, note that len(self) + # is guaranteed to be 2 or more. + return "{%s}" % "+".join(sorted(f"{b}" for b in self)) + + +class FeatureValueConcat(SubstituteBindingsSequence, tuple): + """ + A base feature value that represents the concatenation of two or + more ``FeatureValueTuple`` or ``Variable``. + """ + + def __new__(cls, values): + # If values contains FeatureValueConcats, then collapse them. + values = _flatten(values, FeatureValueConcat) + + # If the resulting list contains no variables, then + # use a simple FeatureValueTuple instead. + if sum(isinstance(v, Variable) for v in values) == 0: + values = _flatten(values, FeatureValueTuple) + return FeatureValueTuple(values) + + # If we contain a single variable, return that variable. + if len(values) == 1: + return list(values)[0] + + # Otherwise, build the FeatureValueConcat. + return tuple.__new__(cls, values) + + def __repr__(self): + # n.b.: len(self) is guaranteed to be 2 or more. + return "(%s)" % "+".join(f"{b}" for b in self) + + +def _flatten(lst, cls): + """ + Helper function -- return a copy of list, with all elements of + type ``cls`` spliced in rather than appended in. + """ + result = [] + for elt in lst: + if isinstance(elt, cls): + result.extend(elt) + else: + result.append(elt) + return result + + +###################################################################### +# Specialized Features +###################################################################### + + +@total_ordering +class Feature: + """ + A feature identifier that's specialized to put additional + constraints, default values, etc. + """ + + def __init__(self, name, default=None, display=None): + assert display in (None, "prefix", "slash") + + self._name = name # [xx] rename to .identifier? + self._default = default # [xx] not implemented yet. + self._display = display + + if self._display == "prefix": + self._sortkey = (-1, self._name) + elif self._display == "slash": + self._sortkey = (1, self._name) + else: + self._sortkey = (0, self._name) + + @property + def name(self): + """The name of this feature.""" + return self._name + + @property + def default(self): + """Default value for this feature.""" + return self._default + + @property + def display(self): + """Custom display location: can be prefix, or slash.""" + return self._display + + def __repr__(self): + return "*%s*" % self.name + + def __lt__(self, other): + if isinstance(other, str): + return True + if not isinstance(other, Feature): + raise_unorderable_types("<", self, other) + return self._sortkey < other._sortkey + + def __eq__(self, other): + return type(self) == type(other) and self._name == other._name + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash(self._name) + + # //////////////////////////////////////////////////////////// + # These can be overridden by subclasses: + # //////////////////////////////////////////////////////////// + + def read_value(self, s, position, reentrances, parser): + return parser.read_value(s, position, reentrances) + + def unify_base_values(self, fval1, fval2, bindings): + """ + If possible, return a single value.. If not, return + the value ``UnificationFailure``. + """ + if fval1 == fval2: + return fval1 + else: + return UnificationFailure + + +class SlashFeature(Feature): + def read_value(self, s, position, reentrances, parser): + return parser.read_partial(s, position, reentrances) + + +class RangeFeature(Feature): + RANGE_RE = re.compile(r"(-?\d+):(-?\d+)") + + def read_value(self, s, position, reentrances, parser): + m = self.RANGE_RE.match(s, position) + if not m: + raise ValueError("range", position) + return (int(m.group(1)), int(m.group(2))), m.end() + + def unify_base_values(self, fval1, fval2, bindings): + if fval1 is None: + return fval2 + if fval2 is None: + return fval1 + rng = max(fval1[0], fval2[0]), min(fval1[1], fval2[1]) + if rng[1] < rng[0]: + return UnificationFailure + return rng + + +SLASH = SlashFeature("slash", default=False, display="slash") +TYPE = Feature("type", display="prefix") + + +###################################################################### +# Specialized Feature Values +###################################################################### + + +@total_ordering +class CustomFeatureValue: + """ + An abstract base class for base values that define a custom + unification method. The custom unification method of + ``CustomFeatureValue`` will be used during unification if: + + - The ``CustomFeatureValue`` is unified with another base value. + - The ``CustomFeatureValue`` is not the value of a customized + ``Feature`` (which defines its own unification method). + + If two ``CustomFeatureValue`` objects are unified with one another + during feature structure unification, then the unified base values + they return *must* be equal; otherwise, an ``AssertionError`` will + be raised. + + Subclasses must define ``unify()``, ``__eq__()`` and ``__lt__()``. + Subclasses may also wish to define ``__hash__()``. + """ + + def unify(self, other): + """ + If this base value unifies with ``other``, then return the + unified value. Otherwise, return ``UnificationFailure``. + """ + raise NotImplementedError("abstract base class") + + def __eq__(self, other): + return NotImplemented + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + return NotImplemented + + def __hash__(self): + raise TypeError("%s objects or unhashable" % self.__class__.__name__) + + +###################################################################### +# Feature Structure Reader +###################################################################### + + +class FeatStructReader: + def __init__( + self, + features=(SLASH, TYPE), + fdict_class=FeatStruct, + flist_class=FeatList, + logic_parser=None, + ): + self._features = {f.name: f for f in features} + self._fdict_class = fdict_class + self._flist_class = flist_class + self._prefix_feature = None + self._slash_feature = None + for feature in features: + if feature.display == "slash": + if self._slash_feature: + raise ValueError("Multiple features w/ display=slash") + self._slash_feature = feature + if feature.display == "prefix": + if self._prefix_feature: + raise ValueError("Multiple features w/ display=prefix") + self._prefix_feature = feature + self._features_with_defaults = [ + feature for feature in features if feature.default is not None + ] + if logic_parser is None: + logic_parser = LogicParser() + self._logic_parser = logic_parser + + def fromstring(self, s, fstruct=None): + """ + Convert a string representation of a feature structure (as + displayed by repr) into a ``FeatStruct``. This process + imposes the following restrictions on the string + representation: + + - Feature names cannot contain any of the following: + whitespace, parentheses, quote marks, equals signs, + dashes, commas, and square brackets. Feature names may + not begin with plus signs or minus signs. + - Only the following basic feature value are supported: + strings, integers, variables, None, and unquoted + alphanumeric strings. + - For reentrant values, the first mention must specify + a reentrance identifier and a value; and any subsequent + mentions must use arrows (``'->'``) to reference the + reentrance identifier. + """ + s = s.strip() + value, position = self.read_partial(s, 0, {}, fstruct) + if position != len(s): + self._error(s, "end of string", position) + return value + + _START_FSTRUCT_RE = re.compile(r"\s*(?:\((\d+)\)\s*)?(\??[\w-]+)?(\[)") + _END_FSTRUCT_RE = re.compile(r"\s*]\s*") + _SLASH_RE = re.compile(r"/") + _FEATURE_NAME_RE = re.compile(r'\s*([+-]?)([^\s\(\)<>"\'\-=\[\],]+)\s*') + _REENTRANCE_RE = re.compile(r"\s*->\s*") + _TARGET_RE = re.compile(r"\s*\((\d+)\)\s*") + _ASSIGN_RE = re.compile(r"\s*=\s*") + _COMMA_RE = re.compile(r"\s*,\s*") + _BARE_PREFIX_RE = re.compile(r"\s*(?:\((\d+)\)\s*)?(\??[\w-]+\s*)()") + # This one is used to distinguish fdicts from flists: + _START_FDICT_RE = re.compile( + r"(%s)|(%s\s*(%s\s*(=|->)|[+-]%s|\]))" + % ( + _BARE_PREFIX_RE.pattern, + _START_FSTRUCT_RE.pattern, + _FEATURE_NAME_RE.pattern, + _FEATURE_NAME_RE.pattern, + ) + ) + + def read_partial(self, s, position=0, reentrances=None, fstruct=None): + """ + Helper function that reads in a feature structure. + + :param s: The string to read. + :param position: The position in the string to start parsing. + :param reentrances: A dictionary from reentrance ids to values. + Defaults to an empty dictionary. + :return: A tuple (val, pos) of the feature structure created by + parsing and the position where the parsed feature structure ends. + :rtype: bool + """ + if reentrances is None: + reentrances = {} + try: + return self._read_partial(s, position, reentrances, fstruct) + except ValueError as e: + if len(e.args) != 2: + raise + self._error(s, *e.args) + + def _read_partial(self, s, position, reentrances, fstruct=None): + # Create the new feature structure + if fstruct is None: + if self._START_FDICT_RE.match(s, position): + fstruct = self._fdict_class() + else: + fstruct = self._flist_class() + + # Read up to the open bracket. + match = self._START_FSTRUCT_RE.match(s, position) + if not match: + match = self._BARE_PREFIX_RE.match(s, position) + if not match: + raise ValueError("open bracket or identifier", position) + position = match.end() + + # If there as an identifier, record it. + if match.group(1): + identifier = match.group(1) + if identifier in reentrances: + raise ValueError("new identifier", match.start(1)) + reentrances[identifier] = fstruct + + if isinstance(fstruct, FeatDict): + fstruct.clear() + return self._read_partial_featdict(s, position, match, reentrances, fstruct) + else: + del fstruct[:] + return self._read_partial_featlist(s, position, match, reentrances, fstruct) + + def _read_partial_featlist(self, s, position, match, reentrances, fstruct): + # Prefix features are not allowed: + if match.group(2): + raise ValueError("open bracket") + # Bare prefixes are not allowed: + if not match.group(3): + raise ValueError("open bracket") + + # Build a list of the features defined by the structure. + while position < len(s): + # Check for the close bracket. + match = self._END_FSTRUCT_RE.match(s, position) + if match is not None: + return fstruct, match.end() + + # Reentances have the form "-> (target)" + match = self._REENTRANCE_RE.match(s, position) + if match: + position = match.end() + match = self._TARGET_RE.match(s, position) + if not match: + raise ValueError("identifier", position) + target = match.group(1) + if target not in reentrances: + raise ValueError("bound identifier", position) + position = match.end() + fstruct.append(reentrances[target]) + + # Anything else is a value. + else: + value, position = self._read_value(0, s, position, reentrances) + fstruct.append(value) + + # If there's a close bracket, handle it at the top of the loop. + if self._END_FSTRUCT_RE.match(s, position): + continue + + # Otherwise, there should be a comma + match = self._COMMA_RE.match(s, position) + if match is None: + raise ValueError("comma", position) + position = match.end() + + # We never saw a close bracket. + raise ValueError("close bracket", position) + + def _read_partial_featdict(self, s, position, match, reentrances, fstruct): + # If there was a prefix feature, record it. + if match.group(2): + if self._prefix_feature is None: + raise ValueError("open bracket or identifier", match.start(2)) + prefixval = match.group(2).strip() + if prefixval.startswith("?"): + prefixval = Variable(prefixval) + fstruct[self._prefix_feature] = prefixval + + # If group 3 is empty, then we just have a bare prefix, so + # we're done. + if not match.group(3): + return self._finalize(s, match.end(), reentrances, fstruct) + + # Build a list of the features defined by the structure. + # Each feature has one of the three following forms: + # name = value + # name -> (target) + # +name + # -name + while position < len(s): + # Use these variables to hold info about each feature: + name = value = None + + # Check for the close bracket. + match = self._END_FSTRUCT_RE.match(s, position) + if match is not None: + return self._finalize(s, match.end(), reentrances, fstruct) + + # Get the feature name's name + match = self._FEATURE_NAME_RE.match(s, position) + if match is None: + raise ValueError("feature name", position) + name = match.group(2) + position = match.end() + + # Check if it's a special feature. + if name[0] == "*" and name[-1] == "*": + name = self._features.get(name[1:-1]) + if name is None: + raise ValueError("known special feature", match.start(2)) + + # Check if this feature has a value already. + if name in fstruct: + raise ValueError("new name", match.start(2)) + + # Boolean value ("+name" or "-name") + if match.group(1) == "+": + value = True + if match.group(1) == "-": + value = False + + # Reentrance link ("-> (target)") + if value is None: + match = self._REENTRANCE_RE.match(s, position) + if match is not None: + position = match.end() + match = self._TARGET_RE.match(s, position) + if not match: + raise ValueError("identifier", position) + target = match.group(1) + if target not in reentrances: + raise ValueError("bound identifier", position) + position = match.end() + value = reentrances[target] + + # Assignment ("= value"). + if value is None: + match = self._ASSIGN_RE.match(s, position) + if match: + position = match.end() + value, position = self._read_value(name, s, position, reentrances) + # None of the above: error. + else: + raise ValueError("equals sign", position) + + # Store the value. + fstruct[name] = value + + # If there's a close bracket, handle it at the top of the loop. + if self._END_FSTRUCT_RE.match(s, position): + continue + + # Otherwise, there should be a comma + match = self._COMMA_RE.match(s, position) + if match is None: + raise ValueError("comma", position) + position = match.end() + + # We never saw a close bracket. + raise ValueError("close bracket", position) + + def _finalize(self, s, pos, reentrances, fstruct): + """ + Called when we see the close brace -- checks for a slash feature, + and adds in default values. + """ + # Add the slash feature (if any) + match = self._SLASH_RE.match(s, pos) + if match: + name = self._slash_feature + v, pos = self._read_value(name, s, match.end(), reentrances) + fstruct[name] = v + ## Add any default features. -- handle in unficiation instead? + # for feature in self._features_with_defaults: + # fstruct.setdefault(feature, feature.default) + # Return the value. + return fstruct, pos + + def _read_value(self, name, s, position, reentrances): + if isinstance(name, Feature): + return name.read_value(s, position, reentrances, self) + else: + return self.read_value(s, position, reentrances) + + def read_value(self, s, position, reentrances): + for handler, regexp in self.VALUE_HANDLERS: + match = regexp.match(s, position) + if match: + handler_func = getattr(self, handler) + return handler_func(s, position, reentrances, match) + raise ValueError("value", position) + + def _error(self, s, expected, position): + lines = s.split("\n") + while position > len(lines[0]): + position -= len(lines.pop(0)) + 1 # +1 for the newline. + estr = ( + "Error parsing feature structure\n " + + lines[0] + + "\n " + + " " * position + + "^ " + + "Expected %s" % expected + ) + raise ValueError(estr) + + # //////////////////////////////////////////////////////////// + # { Value Readers + # //////////////////////////////////////////////////////////// + + #: A table indicating how feature values should be processed. Each + #: entry in the table is a pair (handler, regexp). The first entry + #: with a matching regexp will have its handler called. Handlers + #: should have the following signature:: + #: + #: def handler(s, position, reentrances, match): ... + #: + #: and should return a tuple (value, position), where position is + #: the string position where the value ended. (n.b.: order is + #: important here!) + VALUE_HANDLERS = [ + ("read_fstruct_value", _START_FSTRUCT_RE), + ("read_var_value", re.compile(r"\?[a-zA-Z_][a-zA-Z0-9_]*")), + ("read_str_value", re.compile("[uU]?[rR]?(['\"])")), + ("read_int_value", re.compile(r"-?\d+")), + ("read_sym_value", re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*")), + ( + "read_app_value", + re.compile(r"<(app)\((\?[a-z][a-z]*)\s*," r"\s*(\?[a-z][a-z]*)\)>"), + ), + # ('read_logic_value', re.compile(r'<([^>]*)>')), + # lazily match any character after '<' until we hit a '>' not preceded by '-' + ("read_logic_value", re.compile(r"<(.*?)(?")), + ("read_set_value", re.compile(r"{")), + ("read_tuple_value", re.compile(r"\(")), + ] + + def read_fstruct_value(self, s, position, reentrances, match): + return self.read_partial(s, position, reentrances) + + def read_str_value(self, s, position, reentrances, match): + return read_str(s, position) + + def read_int_value(self, s, position, reentrances, match): + return int(match.group()), match.end() + + # Note: the '?' is included in the variable name. + def read_var_value(self, s, position, reentrances, match): + return Variable(match.group()), match.end() + + _SYM_CONSTS = {"None": None, "True": True, "False": False} + + def read_sym_value(self, s, position, reentrances, match): + val, end = match.group(), match.end() + return self._SYM_CONSTS.get(val, val), end + + def read_app_value(self, s, position, reentrances, match): + """Mainly included for backwards compat.""" + return self._logic_parser.parse("%s(%s)" % match.group(2, 3)), match.end() + + def read_logic_value(self, s, position, reentrances, match): + try: + try: + expr = self._logic_parser.parse(match.group(1)) + except LogicalExpressionException as e: + raise ValueError from e + return expr, match.end() + except ValueError as e: + raise ValueError("logic expression", match.start(1)) from e + + def read_tuple_value(self, s, position, reentrances, match): + return self._read_seq_value( + s, position, reentrances, match, ")", FeatureValueTuple, FeatureValueConcat + ) + + def read_set_value(self, s, position, reentrances, match): + return self._read_seq_value( + s, position, reentrances, match, "}", FeatureValueSet, FeatureValueUnion + ) + + def _read_seq_value( + self, s, position, reentrances, match, close_paren, seq_class, plus_class + ): + """ + Helper function used by read_tuple_value and read_set_value. + """ + cp = re.escape(close_paren) + position = match.end() + # Special syntax of empty tuples: + m = re.compile(r"\s*/?\s*%s" % cp).match(s, position) + if m: + return seq_class(), m.end() + # Read values: + values = [] + seen_plus = False + while True: + # Close paren: return value. + m = re.compile(r"\s*%s" % cp).match(s, position) + if m: + if seen_plus: + return plus_class(values), m.end() + else: + return seq_class(values), m.end() + + # Read the next value. + val, position = self.read_value(s, position, reentrances) + values.append(val) + + # Comma or looking at close paren + m = re.compile(r"\s*(,|\+|(?=%s))\s*" % cp).match(s, position) + if not m: + raise ValueError("',' or '+' or '%s'" % cp, position) + if m.group(1) == "+": + seen_plus = True + position = m.end() + + +###################################################################### +# { Demo +###################################################################### + + +def display_unification(fs1, fs2, indent=" "): + # Print the two input feature structures, side by side. + fs1_lines = ("%s" % fs1).split("\n") + fs2_lines = ("%s" % fs2).split("\n") + if len(fs1_lines) > len(fs2_lines): + blankline = "[" + " " * (len(fs2_lines[0]) - 2) + "]" + fs2_lines += [blankline] * len(fs1_lines) + else: + blankline = "[" + " " * (len(fs1_lines[0]) - 2) + "]" + fs1_lines += [blankline] * len(fs2_lines) + for fs1_line, fs2_line in zip(fs1_lines, fs2_lines): + print(indent + fs1_line + " " + fs2_line) + print(indent + "-" * len(fs1_lines[0]) + " " + "-" * len(fs2_lines[0])) + + linelen = len(fs1_lines[0]) * 2 + 3 + print(indent + "| |".center(linelen)) + print(indent + "+-----UNIFY-----+".center(linelen)) + print(indent + "|".center(linelen)) + print(indent + "V".center(linelen)) + + bindings = {} + + result = fs1.unify(fs2, bindings) + if result is None: + print(indent + "(FAILED)".center(linelen)) + else: + print( + "\n".join(indent + l.center(linelen) for l in ("%s" % result).split("\n")) + ) + if bindings and len(bindings.bound_variables()) > 0: + print(repr(bindings).center(linelen)) + return result + + +def interactive_demo(trace=False): + import random + import sys + + HELP = """ + 1-%d: Select the corresponding feature structure + q: Quit + t: Turn tracing on or off + l: List all feature structures + ?: Help + """ + + print( + """ + This demo will repeatedly present you with a list of feature + structures, and ask you to choose two for unification. Whenever a + new feature structure is generated, it is added to the list of + choices that you can pick from. However, since this can be a + large number of feature structures, the demo will only print out a + random subset for you to choose between at a given time. If you + want to see the complete lists, type "l". For a list of valid + commands, type "?". + """ + ) + print('Press "Enter" to continue...') + sys.stdin.readline() + + fstruct_strings = [ + "[agr=[number=sing, gender=masc]]", + "[agr=[gender=masc, person=3]]", + "[agr=[gender=fem, person=3]]", + "[subj=[agr=(1)[]], agr->(1)]", + "[obj=?x]", + "[subj=?x]", + "[/=None]", + "[/=NP]", + "[cat=NP]", + "[cat=VP]", + "[cat=PP]", + "[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]", + "[gender=masc, agr=?C]", + "[gender=?S, agr=[gender=?S,person=3]]", + ] + + all_fstructs = [ + (i, FeatStruct(fstruct_strings[i])) for i in range(len(fstruct_strings)) + ] + + def list_fstructs(fstructs): + for i, fstruct in fstructs: + print() + lines = ("%s" % fstruct).split("\n") + print("%3d: %s" % (i + 1, lines[0])) + for line in lines[1:]: + print(" " + line) + print() + + while True: + # Pick 5 feature structures at random from the master list. + MAX_CHOICES = 5 + if len(all_fstructs) > MAX_CHOICES: + fstructs = sorted(random.sample(all_fstructs, MAX_CHOICES)) + else: + fstructs = all_fstructs + + print("_" * 75) + + print("Choose two feature structures to unify:") + list_fstructs(fstructs) + + selected = [None, None] + for nth, i in (("First", 0), ("Second", 1)): + while selected[i] is None: + print( + ( + "%s feature structure (1-%d,q,t,l,?): " + % (nth, len(all_fstructs)) + ), + end=" ", + ) + try: + input = sys.stdin.readline().strip() + if input in ("q", "Q", "x", "X"): + return + if input in ("t", "T"): + trace = not trace + print(" Trace = %s" % trace) + continue + if input in ("h", "H", "?"): + print(HELP % len(fstructs)) + continue + if input in ("l", "L"): + list_fstructs(all_fstructs) + continue + num = int(input) - 1 + selected[i] = all_fstructs[num][1] + print() + except Exception: + print("Bad sentence number") + continue + + if trace: + result = selected[0].unify(selected[1], trace=1) + else: + result = display_unification(selected[0], selected[1]) + if result is not None: + for i, fstruct in all_fstructs: + if repr(result) == repr(fstruct): + break + else: + all_fstructs.append((len(all_fstructs), result)) + + print('\nType "Enter" to continue unifying; or "q" to quit.') + input = sys.stdin.readline().strip() + if input in ("q", "Q", "x", "X"): + return + + +def demo(trace=False): + """ + Just for testing + """ + # import random + + # processor breaks with values like '3rd' + fstruct_strings = [ + "[agr=[number=sing, gender=masc]]", + "[agr=[gender=masc, person=3]]", + "[agr=[gender=fem, person=3]]", + "[subj=[agr=(1)[]], agr->(1)]", + "[obj=?x]", + "[subj=?x]", + "[/=None]", + "[/=NP]", + "[cat=NP]", + "[cat=VP]", + "[cat=PP]", + "[subj=[agr=[gender=?y]], obj=[agr=[gender=?y]]]", + "[gender=masc, agr=?C]", + "[gender=?S, agr=[gender=?S,person=3]]", + ] + all_fstructs = [FeatStruct(fss) for fss in fstruct_strings] + # MAX_CHOICES = 5 + # if len(all_fstructs) > MAX_CHOICES: + # fstructs = random.sample(all_fstructs, MAX_CHOICES) + # fstructs.sort() + # else: + # fstructs = all_fstructs + + for fs1 in all_fstructs: + for fs2 in all_fstructs: + print( + "\n*******************\nfs1 is:\n%s\n\nfs2 is:\n%s\n\nresult is:\n%s" + % (fs1, fs2, unify(fs1, fs2)) + ) + + +if __name__ == "__main__": + demo() + +__all__ = [ + "FeatStruct", + "FeatDict", + "FeatList", + "unify", + "subsumes", + "conflicts", + "Feature", + "SlashFeature", + "RangeFeature", + "SLASH", + "TYPE", + "FeatStructReader", +] diff --git a/nltk/grammar.py b/nltk/grammar.py new file mode 100644 index 0000000..d9db617 --- /dev/null +++ b/nltk/grammar.py @@ -0,0 +1,1744 @@ +# Natural Language Toolkit: Context Free Grammars +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# Jason Narad +# Peter Ljunglöf +# Tom Aarsen <> +# URL: +# For license information, see LICENSE.TXT +# + +""" +Basic data classes for representing context free grammars. A +"grammar" specifies which trees can represent the structure of a +given text. Each of these trees is called a "parse tree" for the +text (or simply a "parse"). In a "context free" grammar, the set of +parse trees for any piece of a text can depend only on that piece, and +not on the rest of the text (i.e., the piece's context). Context free +grammars are often used to find possible syntactic structures for +sentences. In this context, the leaves of a parse tree are word +tokens; and the node values are phrasal categories, such as ``NP`` +and ``VP``. + +The ``CFG`` class is used to encode context free grammars. Each +``CFG`` consists of a start symbol and a set of productions. +The "start symbol" specifies the root node value for parse trees. For example, +the start symbol for syntactic parsing is usually ``S``. Start +symbols are encoded using the ``Nonterminal`` class, which is discussed +below. + +A Grammar's "productions" specify what parent-child relationships a parse +tree can contain. Each production specifies that a particular +node can be the parent of a particular set of children. For example, +the production `` -> `` specifies that an ``S`` node can +be the parent of an ``NP`` node and a ``VP`` node. + +Grammar productions are implemented by the ``Production`` class. +Each ``Production`` consists of a left hand side and a right hand +side. The "left hand side" is a ``Nonterminal`` that specifies the +node type for a potential parent; and the "right hand side" is a list +that specifies allowable children for that parent. This lists +consists of ``Nonterminals`` and text types: each ``Nonterminal`` +indicates that the corresponding child may be a ``TreeToken`` with the +specified node type; and each text type indicates that the +corresponding child may be a ``Token`` with the with that type. + +The ``Nonterminal`` class is used to distinguish node values from leaf +values. This prevents the grammar from accidentally using a leaf +value (such as the English word "A") as the node of a subtree. Within +a ``CFG``, all node values are wrapped in the ``Nonterminal`` +class. Note, however, that the trees that are specified by the grammar do +*not* include these ``Nonterminal`` wrappers. + +Grammars can also be given a more procedural interpretation. According to +this interpretation, a Grammar specifies any tree structure *tree* that +can be produced by the following procedure: + +| Set tree to the start symbol +| Repeat until tree contains no more nonterminal leaves: +| Choose a production prod with whose left hand side +| lhs is a nonterminal leaf of tree. +| Replace the nonterminal leaf with a subtree, whose node +| value is the value wrapped by the nonterminal lhs, and +| whose children are the right hand side of prod. + +The operation of replacing the left hand side (*lhs*) of a production +with the right hand side (*rhs*) in a tree (*tree*) is known as +"expanding" *lhs* to *rhs* in *tree*. +""" +import re +from collections import deque +from functools import total_ordering + +from nltk.featstruct import SLASH, TYPE, FeatDict, FeatStruct, FeatStructReader +from nltk.internals import raise_unorderable_types +from nltk.probability import ImmutableProbabilisticMixIn +from nltk.util import invert_graph, transitive_closure + +################################################################# +# Nonterminal +################################################################# + + +@total_ordering +class Nonterminal: + """ + A non-terminal symbol for a context free grammar. ``Nonterminal`` + is a wrapper class for node values; it is used by ``Production`` + objects to distinguish node values from leaf values. + The node value that is wrapped by a ``Nonterminal`` is known as its + "symbol". Symbols are typically strings representing phrasal + categories (such as ``"NP"`` or ``"VP"``). However, more complex + symbol types are sometimes used (e.g., for lexicalized grammars). + Since symbols are node values, they must be immutable and + hashable. Two ``Nonterminals`` are considered equal if their + symbols are equal. + + :see: ``CFG``, ``Production`` + :type _symbol: any + :ivar _symbol: The node value corresponding to this + ``Nonterminal``. This value must be immutable and hashable. + """ + + def __init__(self, symbol): + """ + Construct a new non-terminal from the given symbol. + + :type symbol: any + :param symbol: The node value corresponding to this + ``Nonterminal``. This value must be immutable and + hashable. + """ + self._symbol = symbol + + def symbol(self): + """ + Return the node value corresponding to this ``Nonterminal``. + + :rtype: (any) + """ + return self._symbol + + def __eq__(self, other): + """ + Return True if this non-terminal is equal to ``other``. In + particular, return True if ``other`` is a ``Nonterminal`` + and this non-terminal's symbol is equal to ``other`` 's symbol. + + :rtype: bool + """ + return type(self) == type(other) and self._symbol == other._symbol + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, Nonterminal): + raise_unorderable_types("<", self, other) + return self._symbol < other._symbol + + def __hash__(self): + return hash(self._symbol) + + def __repr__(self): + """ + Return a string representation for this ``Nonterminal``. + + :rtype: str + """ + if isinstance(self._symbol, str): + return "%s" % self._symbol + else: + return "%s" % repr(self._symbol) + + def __str__(self): + """ + Return a string representation for this ``Nonterminal``. + + :rtype: str + """ + if isinstance(self._symbol, str): + return "%s" % self._symbol + else: + return "%s" % repr(self._symbol) + + def __div__(self, rhs): + """ + Return a new nonterminal whose symbol is ``A/B``, where ``A`` is + the symbol for this nonterminal, and ``B`` is the symbol for rhs. + + :param rhs: The nonterminal used to form the right hand side + of the new nonterminal. + :type rhs: Nonterminal + :rtype: Nonterminal + """ + return Nonterminal(f"{self._symbol}/{rhs._symbol}") + + def __truediv__(self, rhs): + """ + Return a new nonterminal whose symbol is ``A/B``, where ``A`` is + the symbol for this nonterminal, and ``B`` is the symbol for rhs. + This function allows use of the slash ``/`` operator with + the future import of division. + + :param rhs: The nonterminal used to form the right hand side + of the new nonterminal. + :type rhs: Nonterminal + :rtype: Nonterminal + """ + return self.__div__(rhs) + + +def nonterminals(symbols): + """ + Given a string containing a list of symbol names, return a list of + ``Nonterminals`` constructed from those symbols. + + :param symbols: The symbol name string. This string can be + delimited by either spaces or commas. + :type symbols: str + :return: A list of ``Nonterminals`` constructed from the symbol + names given in ``symbols``. The ``Nonterminals`` are sorted + in the same order as the symbols names. + :rtype: list(Nonterminal) + """ + if "," in symbols: + symbol_list = symbols.split(",") + else: + symbol_list = symbols.split() + return [Nonterminal(s.strip()) for s in symbol_list] + + +class FeatStructNonterminal(FeatDict, Nonterminal): + """A feature structure that's also a nonterminal. It acts as its + own symbol, and automatically freezes itself when hashed.""" + + def __hash__(self): + self.freeze() + return FeatStruct.__hash__(self) + + def symbol(self): + return self + + +def is_nonterminal(item): + """ + :return: True if the item is a ``Nonterminal``. + :rtype: bool + """ + return isinstance(item, Nonterminal) + + +################################################################# +# Terminals +################################################################# + + +def is_terminal(item): + """ + Return True if the item is a terminal, which currently is + if it is hashable and not a ``Nonterminal``. + + :rtype: bool + """ + return hasattr(item, "__hash__") and not isinstance(item, Nonterminal) + + +################################################################# +# Productions +################################################################# + + +@total_ordering +class Production: + """ + A grammar production. Each production maps a single symbol + on the "left-hand side" to a sequence of symbols on the + "right-hand side". (In the case of context-free productions, + the left-hand side must be a ``Nonterminal``, and the right-hand + side is a sequence of terminals and ``Nonterminals``.) + "terminals" can be any immutable hashable object that is + not a ``Nonterminal``. Typically, terminals are strings + representing words, such as ``"dog"`` or ``"under"``. + + :see: ``CFG`` + :see: ``DependencyGrammar`` + :see: ``Nonterminal`` + :type _lhs: Nonterminal + :ivar _lhs: The left-hand side of the production. + :type _rhs: tuple(Nonterminal, terminal) + :ivar _rhs: The right-hand side of the production. + """ + + def __init__(self, lhs, rhs): + """ + Construct a new ``Production``. + + :param lhs: The left-hand side of the new ``Production``. + :type lhs: Nonterminal + :param rhs: The right-hand side of the new ``Production``. + :type rhs: sequence(Nonterminal and terminal) + """ + if isinstance(rhs, str): + raise TypeError( + "production right hand side should be a list, " "not a string" + ) + self._lhs = lhs + self._rhs = tuple(rhs) + + def lhs(self): + """ + Return the left-hand side of this ``Production``. + + :rtype: Nonterminal + """ + return self._lhs + + def rhs(self): + """ + Return the right-hand side of this ``Production``. + + :rtype: sequence(Nonterminal and terminal) + """ + return self._rhs + + def __len__(self): + """ + Return the length of the right-hand side. + + :rtype: int + """ + return len(self._rhs) + + def is_nonlexical(self): + """ + Return True if the right-hand side only contains ``Nonterminals`` + + :rtype: bool + """ + return all(is_nonterminal(n) for n in self._rhs) + + def is_lexical(self): + """ + Return True if the right-hand contain at least one terminal token. + + :rtype: bool + """ + return not self.is_nonlexical() + + def __str__(self): + """ + Return a verbose string representation of the ``Production``. + + :rtype: str + """ + result = "%s -> " % repr(self._lhs) + result += " ".join(repr(el) for el in self._rhs) + return result + + def __repr__(self): + """ + Return a concise string representation of the ``Production``. + + :rtype: str + """ + return "%s" % self + + def __eq__(self, other): + """ + Return True if this ``Production`` is equal to ``other``. + + :rtype: bool + """ + return ( + type(self) == type(other) + and self._lhs == other._lhs + and self._rhs == other._rhs + ) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, Production): + raise_unorderable_types("<", self, other) + return (self._lhs, self._rhs) < (other._lhs, other._rhs) + + def __hash__(self): + """ + Return a hash value for the ``Production``. + + :rtype: int + """ + return hash((self._lhs, self._rhs)) + + +class DependencyProduction(Production): + """ + A dependency grammar production. Each production maps a single + head word to an unordered list of one or more modifier words. + """ + + def __str__(self): + """ + Return a verbose string representation of the ``DependencyProduction``. + + :rtype: str + """ + result = f"'{self._lhs}' ->" + for elt in self._rhs: + result += f" '{elt}'" + return result + + +class ProbabilisticProduction(Production, ImmutableProbabilisticMixIn): + """ + A probabilistic context free grammar production. + A PCFG ``ProbabilisticProduction`` is essentially just a ``Production`` that + has an associated probability, which represents how likely it is that + this production will be used. In particular, the probability of a + ``ProbabilisticProduction`` records the likelihood that its right-hand side is + the correct instantiation for any given occurrence of its left-hand side. + + :see: ``Production`` + """ + + def __init__(self, lhs, rhs, **prob): + """ + Construct a new ``ProbabilisticProduction``. + + :param lhs: The left-hand side of the new ``ProbabilisticProduction``. + :type lhs: Nonterminal + :param rhs: The right-hand side of the new ``ProbabilisticProduction``. + :type rhs: sequence(Nonterminal and terminal) + :param prob: Probability parameters of the new ``ProbabilisticProduction``. + """ + ImmutableProbabilisticMixIn.__init__(self, **prob) + Production.__init__(self, lhs, rhs) + + def __str__(self): + return super().__str__() + ( + " [1.0]" if (self.prob() == 1.0) else " [%g]" % self.prob() + ) + + def __eq__(self, other): + return ( + type(self) == type(other) + and self._lhs == other._lhs + and self._rhs == other._rhs + and self.prob() == other.prob() + ) + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self._lhs, self._rhs, self.prob())) + + +################################################################# +# Grammars +################################################################# + + +class CFG: + """ + A context-free grammar. A grammar consists of a start state and + a set of productions. The set of terminals and nonterminals is + implicitly specified by the productions. + + If you need efficient key-based access to productions, you + can use a subclass to implement it. + """ + + def __init__(self, start, productions, calculate_leftcorners=True): + """ + Create a new context-free grammar, from the given start state + and set of ``Production`` instances. + + :param start: The start symbol + :type start: Nonterminal + :param productions: The list of productions that defines the grammar + :type productions: list(Production) + :param calculate_leftcorners: False if we don't want to calculate the + leftcorner relation. In that case, some optimized chart parsers won't work. + :type calculate_leftcorners: bool + """ + if not is_nonterminal(start): + raise TypeError( + "start should be a Nonterminal object," + " not a %s" % type(start).__name__ + ) + + self._start = start + self._productions = productions + self._categories = {prod.lhs() for prod in productions} + self._calculate_indexes() + self._calculate_grammar_forms() + if calculate_leftcorners: + self._calculate_leftcorners() + + def _calculate_indexes(self): + self._lhs_index = {} + self._rhs_index = {} + self._empty_index = {} + self._lexical_index = {} + for prod in self._productions: + # Left hand side. + lhs = prod._lhs + if lhs not in self._lhs_index: + self._lhs_index[lhs] = [] + self._lhs_index[lhs].append(prod) + if prod._rhs: + # First item in right hand side. + rhs0 = prod._rhs[0] + if rhs0 not in self._rhs_index: + self._rhs_index[rhs0] = [] + self._rhs_index[rhs0].append(prod) + else: + # The right hand side is empty. + self._empty_index[prod.lhs()] = prod + # Lexical tokens in the right hand side. + for token in prod._rhs: + if is_terminal(token): + self._lexical_index.setdefault(token, set()).add(prod) + + def _calculate_leftcorners(self): + # Calculate leftcorner relations, for use in optimized parsing. + self._immediate_leftcorner_categories = {cat: {cat} for cat in self._categories} + self._immediate_leftcorner_words = {cat: set() for cat in self._categories} + for prod in self.productions(): + if len(prod) > 0: + cat, left = prod.lhs(), prod.rhs()[0] + if is_nonterminal(left): + self._immediate_leftcorner_categories[cat].add(left) + else: + self._immediate_leftcorner_words[cat].add(left) + + lc = transitive_closure(self._immediate_leftcorner_categories, reflexive=True) + self._leftcorners = lc + self._leftcorner_parents = invert_graph(lc) + + nr_leftcorner_categories = sum( + map(len, self._immediate_leftcorner_categories.values()) + ) + nr_leftcorner_words = sum(map(len, self._immediate_leftcorner_words.values())) + if nr_leftcorner_words > nr_leftcorner_categories > 10000: + # If the grammar is big, the leftcorner-word dictionary will be too large. + # In that case it is better to calculate the relation on demand. + self._leftcorner_words = None + return + + self._leftcorner_words = {} + for cat in self._leftcorners: + lefts = self._leftcorners[cat] + lc = self._leftcorner_words[cat] = set() + for left in lefts: + lc.update(self._immediate_leftcorner_words.get(left, set())) + + @classmethod + def fromstring(cls, input, encoding=None): + """ + Return the grammar instance corresponding to the input string(s). + + :param input: a grammar, either in the form of a string or as a list of strings. + """ + start, productions = read_grammar( + input, standard_nonterm_parser, encoding=encoding + ) + return cls(start, productions) + + def start(self): + """ + Return the start symbol of the grammar + + :rtype: Nonterminal + """ + return self._start + + # tricky to balance readability and efficiency here! + # can't use set operations as they don't preserve ordering + def productions(self, lhs=None, rhs=None, empty=False): + """ + Return the grammar productions, filtered by the left-hand side + or the first item in the right-hand side. + + :param lhs: Only return productions with the given left-hand side. + :param rhs: Only return productions with the given first item + in the right-hand side. + :param empty: Only return productions with an empty right-hand side. + :return: A list of productions matching the given constraints. + :rtype: list(Production) + """ + if rhs and empty: + raise ValueError( + "You cannot select empty and non-empty " "productions at the same time." + ) + + # no constraints so return everything + if not lhs and not rhs: + if not empty: + return self._productions + else: + return self._empty_index.values() + + # only lhs specified so look up its index + elif lhs and not rhs: + if not empty: + return self._lhs_index.get(lhs, []) + elif lhs in self._empty_index: + return [self._empty_index[lhs]] + else: + return [] + + # only rhs specified so look up its index + elif rhs and not lhs: + return self._rhs_index.get(rhs, []) + + # intersect + else: + return [ + prod + for prod in self._lhs_index.get(lhs, []) + if prod in self._rhs_index.get(rhs, []) + ] + + def leftcorners(self, cat): + """ + Return the set of all nonterminals that the given nonterminal + can start with, including itself. + + This is the reflexive, transitive closure of the immediate + leftcorner relation: (A > B) iff (A -> B beta) + + :param cat: the parent of the leftcorners + :type cat: Nonterminal + :return: the set of all leftcorners + :rtype: set(Nonterminal) + """ + return self._leftcorners.get(cat, {cat}) + + def is_leftcorner(self, cat, left): + """ + True if left is a leftcorner of cat, where left can be a + terminal or a nonterminal. + + :param cat: the parent of the leftcorner + :type cat: Nonterminal + :param left: the suggested leftcorner + :type left: Terminal or Nonterminal + :rtype: bool + """ + if is_nonterminal(left): + return left in self.leftcorners(cat) + elif self._leftcorner_words: + return left in self._leftcorner_words.get(cat, set()) + else: + return any( + left in self._immediate_leftcorner_words.get(parent, set()) + for parent in self.leftcorners(cat) + ) + + def leftcorner_parents(self, cat): + """ + Return the set of all nonterminals for which the given category + is a left corner. This is the inverse of the leftcorner relation. + + :param cat: the suggested leftcorner + :type cat: Nonterminal + :return: the set of all parents to the leftcorner + :rtype: set(Nonterminal) + """ + return self._leftcorner_parents.get(cat, {cat}) + + def check_coverage(self, tokens): + """ + Check whether the grammar rules cover the given list of tokens. + If not, then raise an exception. + + :type tokens: list(str) + """ + missing = [tok for tok in tokens if not self._lexical_index.get(tok)] + if missing: + missing = ", ".join(f"{w!r}" for w in missing) + raise ValueError( + "Grammar does not cover some of the " "input words: %r." % missing + ) + + def _calculate_grammar_forms(self): + """ + Pre-calculate of which form(s) the grammar is. + """ + prods = self._productions + self._is_lexical = all(p.is_lexical() for p in prods) + self._is_nonlexical = all(p.is_nonlexical() for p in prods if len(p) != 1) + self._min_len = min(len(p) for p in prods) + self._max_len = max(len(p) for p in prods) + self._all_unary_are_lexical = all(p.is_lexical() for p in prods if len(p) == 1) + + def is_lexical(self): + """ + Return True if all productions are lexicalised. + """ + return self._is_lexical + + def is_nonlexical(self): + """ + Return True if all lexical rules are "preterminals", that is, + unary rules which can be separated in a preprocessing step. + + This means that all productions are of the forms + A -> B1 ... Bn (n>=0), or A -> "s". + + Note: is_lexical() and is_nonlexical() are not opposites. + There are grammars which are neither, and grammars which are both. + """ + return self._is_nonlexical + + def min_len(self): + """ + Return the right-hand side length of the shortest grammar production. + """ + return self._min_len + + def max_len(self): + """ + Return the right-hand side length of the longest grammar production. + """ + return self._max_len + + def is_nonempty(self): + """ + Return True if there are no empty productions. + """ + return self._min_len > 0 + + def is_binarised(self): + """ + Return True if all productions are at most binary. + Note that there can still be empty and unary productions. + """ + return self._max_len <= 2 + + def is_flexible_chomsky_normal_form(self): + """ + Return True if all productions are of the forms + A -> B C, A -> B, or A -> "s". + """ + return self.is_nonempty() and self.is_nonlexical() and self.is_binarised() + + def is_chomsky_normal_form(self): + """ + Return True if the grammar is of Chomsky Normal Form, i.e. all productions + are of the form A -> B C, or A -> "s". + """ + return self.is_flexible_chomsky_normal_form() and self._all_unary_are_lexical + + def chomsky_normal_form(self, new_token_padding="@$@", flexible=False): + """ + Returns a new Grammar that is in chomsky normal + + :param: new_token_padding + Customise new rule formation during binarisation + """ + if self.is_chomsky_normal_form(): + return self + if self.productions(empty=True): + raise ValueError( + "Grammar has Empty rules. " "Cannot deal with them at the moment" + ) + + step1 = CFG.eliminate_start(self) + step2 = CFG.binarize(step1, new_token_padding) + step3 = CFG.remove_mixed_rules(step2, new_token_padding) + if flexible: + return step3 + step4 = CFG.remove_unitary_rules(step3) + return CFG(step4.start(), list(set(step4.productions()))) + + @classmethod + def remove_unitary_rules(cls, grammar): + """ + Remove nonlexical unitary rules and convert them to + lexical + """ + result = [] + unitary = deque([]) + for rule in grammar.productions(): + if len(rule) == 1 and rule.is_nonlexical(): + unitary.append(rule) + else: + result.append(rule) + + while unitary: + rule = unitary.popleft() + for item in grammar.productions(lhs=rule.rhs()[0]): + new_rule = Production(rule.lhs(), item.rhs()) + if len(new_rule) != 1 or new_rule.is_lexical(): + result.append(new_rule) + else: + unitary.append(new_rule) + + n_grammar = CFG(grammar.start(), result) + return n_grammar + + @classmethod + def binarize(cls, grammar, padding="@$@"): + """ + Convert all non-binary rules into binary by introducing + new tokens. + Example:: + + Original: + A => B C D + After Conversion: + A => B A@$@B + A@$@B => C D + """ + result = [] + + for rule in grammar.productions(): + if len(rule.rhs()) > 2: + # this rule needs to be broken down + left_side = rule.lhs() + for k in range(0, len(rule.rhs()) - 2): + tsym = rule.rhs()[k] + new_sym = Nonterminal(left_side.symbol() + padding + tsym.symbol()) + new_production = Production(left_side, (tsym, new_sym)) + left_side = new_sym + result.append(new_production) + last_prd = Production(left_side, rule.rhs()[-2:]) + result.append(last_prd) + else: + result.append(rule) + + n_grammar = CFG(grammar.start(), result) + return n_grammar + + @classmethod + def eliminate_start(cls, grammar): + """ + Eliminate start rule in case it appears on RHS + Example: S -> S0 S1 and S0 -> S1 S + Then another rule S0_Sigma -> S is added + """ + start = grammar.start() + result = [] + need_to_add = None + for rule in grammar.productions(): + if start in rule.rhs(): + need_to_add = True + result.append(rule) + if need_to_add: + start = Nonterminal("S0_SIGMA") + result.append(Production(start, [grammar.start()])) + n_grammar = CFG(start, result) + return n_grammar + return grammar + + @classmethod + def remove_mixed_rules(cls, grammar, padding="@$@"): + """ + Convert all mixed rules containing terminals and non-terminals + into dummy non-terminals. + Example:: + + Original: + A => term B + After Conversion: + A => TERM@$@TERM B + TERM@$@TERM => term + """ + result = [] + dummy_nonterms = {} + for rule in grammar.productions(): + if not rule.is_lexical() or len(rule.rhs()) <= 1: + result.append(rule) + continue + + new_rhs = [] + for item in rule.rhs(): + if is_nonterminal(item): + new_rhs.append(item) + else: + if item not in dummy_nonterms: + sanitized_term = "".join( + _STANDARD_NONTERM_RE.findall(item.upper()) + ) + dummy_nonterm_symbol = ( + f"{sanitized_term}{padding}{sanitized_term}" + ) + dummy_nonterms[item] = Nonterminal(dummy_nonterm_symbol) + + new_rhs.append(dummy_nonterms[item]) + result.append(Production(dummy_nonterms[item], rhs=[item])) + + result.append(Production(rule.lhs(), new_rhs)) + + n_grammar = CFG(grammar.start(), result) + return n_grammar + + def __repr__(self): + return "" % len(self._productions) + + def __str__(self): + result = "Grammar with %d productions" % len(self._productions) + result += " (start state = %r)" % self._start + for production in self._productions: + result += "\n %s" % production + return result + + +class FeatureGrammar(CFG): + """ + A feature-based grammar. This is equivalent to a + ``CFG`` whose nonterminals are all + ``FeatStructNonterminal``. + + A grammar consists of a start state and a set of + productions. The set of terminals and nonterminals + is implicitly specified by the productions. + """ + + def __init__(self, start, productions): + """ + Create a new feature-based grammar, from the given start + state and set of ``Productions``. + + :param start: The start symbol + :type start: FeatStructNonterminal + :param productions: The list of productions that defines the grammar + :type productions: list(Production) + """ + CFG.__init__(self, start, productions) + + # The difference with CFG is that the productions are + # indexed on the TYPE feature of the nonterminals. + # This is calculated by the method _get_type_if_possible(). + + def _calculate_indexes(self): + self._lhs_index = {} + self._rhs_index = {} + self._empty_index = {} + self._empty_productions = [] + self._lexical_index = {} + for prod in self._productions: + # Left hand side. + lhs = self._get_type_if_possible(prod._lhs) + if lhs not in self._lhs_index: + self._lhs_index[lhs] = [] + self._lhs_index[lhs].append(prod) + if prod._rhs: + # First item in right hand side. + rhs0 = self._get_type_if_possible(prod._rhs[0]) + if rhs0 not in self._rhs_index: + self._rhs_index[rhs0] = [] + self._rhs_index[rhs0].append(prod) + else: + # The right hand side is empty. + if lhs not in self._empty_index: + self._empty_index[lhs] = [] + self._empty_index[lhs].append(prod) + self._empty_productions.append(prod) + # Lexical tokens in the right hand side. + for token in prod._rhs: + if is_terminal(token): + self._lexical_index.setdefault(token, set()).add(prod) + + @classmethod + def fromstring( + cls, input, features=None, logic_parser=None, fstruct_reader=None, encoding=None + ): + """ + Return a feature structure based grammar. + + :param input: a grammar, either in the form of a string or else + as a list of strings. + :param features: a tuple of features (default: SLASH, TYPE) + :param logic_parser: a parser for lambda-expressions, + by default, ``LogicParser()`` + :param fstruct_reader: a feature structure parser + (only if features and logic_parser is None) + """ + if features is None: + features = (SLASH, TYPE) + + if fstruct_reader is None: + fstruct_reader = FeatStructReader( + features, FeatStructNonterminal, logic_parser=logic_parser + ) + elif logic_parser is not None: + raise Exception( + "'logic_parser' and 'fstruct_reader' must " "not both be set" + ) + + start, productions = read_grammar( + input, fstruct_reader.read_partial, encoding=encoding + ) + return cls(start, productions) + + def productions(self, lhs=None, rhs=None, empty=False): + """ + Return the grammar productions, filtered by the left-hand side + or the first item in the right-hand side. + + :param lhs: Only return productions with the given left-hand side. + :param rhs: Only return productions with the given first item + in the right-hand side. + :param empty: Only return productions with an empty right-hand side. + :rtype: list(Production) + """ + if rhs and empty: + raise ValueError( + "You cannot select empty and non-empty " "productions at the same time." + ) + + # no constraints so return everything + if not lhs and not rhs: + if empty: + return self._empty_productions + else: + return self._productions + + # only lhs specified so look up its index + elif lhs and not rhs: + if empty: + return self._empty_index.get(self._get_type_if_possible(lhs), []) + else: + return self._lhs_index.get(self._get_type_if_possible(lhs), []) + + # only rhs specified so look up its index + elif rhs and not lhs: + return self._rhs_index.get(self._get_type_if_possible(rhs), []) + + # intersect + else: + return [ + prod + for prod in self._lhs_index.get(self._get_type_if_possible(lhs), []) + if prod in self._rhs_index.get(self._get_type_if_possible(rhs), []) + ] + + def leftcorners(self, cat): + """ + Return the set of all words that the given category can start with. + Also called the "first set" in compiler construction. + """ + raise NotImplementedError("Not implemented yet") + + def leftcorner_parents(self, cat): + """ + Return the set of all categories for which the given category + is a left corner. + """ + raise NotImplementedError("Not implemented yet") + + def _get_type_if_possible(self, item): + """ + Helper function which returns the ``TYPE`` feature of the ``item``, + if it exists, otherwise it returns the ``item`` itself + """ + if isinstance(item, dict) and TYPE in item: + return FeatureValueType(item[TYPE]) + else: + return item + + +@total_ordering +class FeatureValueType: + """ + A helper class for ``FeatureGrammars``, designed to be different + from ordinary strings. This is to stop the ``FeatStruct`` + ``FOO[]`` from being compare equal to the terminal "FOO". + """ + + def __init__(self, value): + self._value = value + + def __repr__(self): + return "<%s>" % self._value + + def __eq__(self, other): + return type(self) == type(other) and self._value == other._value + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, FeatureValueType): + raise_unorderable_types("<", self, other) + return self._value < other._value + + def __hash__(self): + return hash(self._value) + + +class DependencyGrammar: + """ + A dependency grammar. A DependencyGrammar consists of a set of + productions. Each production specifies a head/modifier relationship + between a pair of words. + """ + + def __init__(self, productions): + """ + Create a new dependency grammar, from the set of ``Productions``. + + :param productions: The list of productions that defines the grammar + :type productions: list(Production) + """ + self._productions = productions + + @classmethod + def fromstring(cls, input): + productions = [] + for linenum, line in enumerate(input.split("\n")): + line = line.strip() + if line.startswith("#") or line == "": + continue + try: + productions += _read_dependency_production(line) + except ValueError as e: + raise ValueError(f"Unable to parse line {linenum}: {line}") from e + if len(productions) == 0: + raise ValueError("No productions found!") + return cls(productions) + + def contains(self, head, mod): + """ + :param head: A head word. + :type head: str + :param mod: A mod word, to test as a modifier of 'head'. + :type mod: str + + :return: true if this ``DependencyGrammar`` contains a + ``DependencyProduction`` mapping 'head' to 'mod'. + :rtype: bool + """ + for production in self._productions: + for possibleMod in production._rhs: + if production._lhs == head and possibleMod == mod: + return True + return False + + def __contains__(self, head_mod): + """ + Return True if this ``DependencyGrammar`` contains a + ``DependencyProduction`` mapping 'head' to 'mod'. + + :param head_mod: A tuple of a head word and a mod word, + to test as a modifier of 'head'. + :type head: Tuple[str, str] + :rtype: bool + """ + try: + head, mod = head_mod + except ValueError as e: + raise ValueError( + "Must use a tuple of strings, e.g. `('price', 'of') in grammar`" + ) from e + return self.contains(head, mod) + + # # should be rewritten, the set comp won't work in all comparisons + # def contains_exactly(self, head, modlist): + # for production in self._productions: + # if(len(production._rhs) == len(modlist)): + # if(production._lhs == head): + # set1 = Set(production._rhs) + # set2 = Set(modlist) + # if(set1 == set2): + # return True + # return False + + def __str__(self): + """ + Return a verbose string representation of the ``DependencyGrammar`` + + :rtype: str + """ + str = "Dependency grammar with %d productions" % len(self._productions) + for production in self._productions: + str += "\n %s" % production + return str + + def __repr__(self): + """ + Return a concise string representation of the ``DependencyGrammar`` + """ + return "Dependency grammar with %d productions" % len(self._productions) + + +class ProbabilisticDependencyGrammar: + """ """ + + def __init__(self, productions, events, tags): + self._productions = productions + self._events = events + self._tags = tags + + def contains(self, head, mod): + """ + Return True if this ``DependencyGrammar`` contains a + ``DependencyProduction`` mapping 'head' to 'mod'. + + :param head: A head word. + :type head: str + :param mod: A mod word, to test as a modifier of 'head'. + :type mod: str + :rtype: bool + """ + for production in self._productions: + for possibleMod in production._rhs: + if production._lhs == head and possibleMod == mod: + return True + return False + + def __str__(self): + """ + Return a verbose string representation of the ``ProbabilisticDependencyGrammar`` + + :rtype: str + """ + str = "Statistical dependency grammar with %d productions" % len( + self._productions + ) + for production in self._productions: + str += "\n %s" % production + str += "\nEvents:" + for event in self._events: + str += "\n %d:%s" % (self._events[event], event) + str += "\nTags:" + for tag_word in self._tags: + str += f"\n {tag_word}:\t({self._tags[tag_word]})" + return str + + def __repr__(self): + """ + Return a concise string representation of the ``ProbabilisticDependencyGrammar`` + """ + return "Statistical Dependency grammar with %d productions" % len( + self._productions + ) + + +class PCFG(CFG): + """ + A probabilistic context-free grammar. A PCFG consists of a + start state and a set of productions with probabilities. The set of + terminals and nonterminals is implicitly specified by the productions. + + PCFG productions use the ``ProbabilisticProduction`` class. + ``PCFGs`` impose the constraint that the set of productions with + any given left-hand-side must have probabilities that sum to 1 + (allowing for a small margin of error). + + If you need efficient key-based access to productions, you can use + a subclass to implement it. + + :type EPSILON: float + :cvar EPSILON: The acceptable margin of error for checking that + productions with a given left-hand side have probabilities + that sum to 1. + """ + + EPSILON = 0.01 + + def __init__(self, start, productions, calculate_leftcorners=True): + """ + Create a new context-free grammar, from the given start state + and set of ``ProbabilisticProductions``. + + :param start: The start symbol + :type start: Nonterminal + :param productions: The list of productions that defines the grammar + :type productions: list(Production) + :raise ValueError: if the set of productions with any left-hand-side + do not have probabilities that sum to a value within + EPSILON of 1. + :param calculate_leftcorners: False if we don't want to calculate the + leftcorner relation. In that case, some optimized chart parsers won't work. + :type calculate_leftcorners: bool + """ + CFG.__init__(self, start, productions, calculate_leftcorners) + + # Make sure that the probabilities sum to one. + probs = {} + for production in productions: + probs[production.lhs()] = probs.get(production.lhs(), 0) + production.prob() + for lhs, p in probs.items(): + if not ((1 - PCFG.EPSILON) < p < (1 + PCFG.EPSILON)): + raise ValueError("Productions for %r do not sum to 1" % lhs) + + @classmethod + def fromstring(cls, input, encoding=None): + """ + Return a probabilistic context-free grammar corresponding to the + input string(s). + + :param input: a grammar, either in the form of a string or else + as a list of strings. + """ + start, productions = read_grammar( + input, standard_nonterm_parser, probabilistic=True, encoding=encoding + ) + return cls(start, productions) + + +################################################################# +# Inducing Grammars +################################################################# + +# Contributed by Nathan Bodenstab + + +def induce_pcfg(start, productions): + r""" + Induce a PCFG grammar from a list of productions. + + The probability of a production A -> B C in a PCFG is: + + | count(A -> B C) + | P(B, C | A) = --------------- where \* is any right hand side + | count(A -> \*) + + :param start: The start symbol + :type start: Nonterminal + :param productions: The list of productions that defines the grammar + :type productions: list(Production) + """ + # Production count: the number of times a given production occurs + pcount = {} + + # LHS-count: counts the number of times a given lhs occurs + lcount = {} + + for prod in productions: + lcount[prod.lhs()] = lcount.get(prod.lhs(), 0) + 1 + pcount[prod] = pcount.get(prod, 0) + 1 + + prods = [ + ProbabilisticProduction(p.lhs(), p.rhs(), prob=pcount[p] / lcount[p.lhs()]) + for p in pcount + ] + return PCFG(start, prods) + + +################################################################# +# Helper functions for reading productions +################################################################# + + +def _read_cfg_production(input): + """ + Return a list of context-free ``Productions``. + """ + return _read_production(input, standard_nonterm_parser) + + +def _read_pcfg_production(input): + """ + Return a list of PCFG ``ProbabilisticProductions``. + """ + return _read_production(input, standard_nonterm_parser, probabilistic=True) + + +def _read_fcfg_production(input, fstruct_reader): + """ + Return a list of feature-based ``Productions``. + """ + return _read_production(input, fstruct_reader) + + +# Parsing generic grammars + +_ARROW_RE = re.compile(r"\s* -> \s*", re.VERBOSE) +_PROBABILITY_RE = re.compile(r"( \[ [\d\.]+ \] ) \s*", re.VERBOSE) +_TERMINAL_RE = re.compile(r'( "[^"]*" | \'[^\']*\' ) \s*', re.VERBOSE) +_DISJUNCTION_RE = re.compile(r"\| \s*", re.VERBOSE) + + +def _read_production(line, nonterm_parser, probabilistic=False): + """ + Parse a grammar rule, given as a string, and return + a list of productions. + """ + pos = 0 + + # Parse the left-hand side. + lhs, pos = nonterm_parser(line, pos) + + # Skip over the arrow. + m = _ARROW_RE.match(line, pos) + if not m: + raise ValueError("Expected an arrow") + pos = m.end() + + # Parse the right hand side. + probabilities = [0.0] + rhsides = [[]] + while pos < len(line): + # Probability. + m = _PROBABILITY_RE.match(line, pos) + if probabilistic and m: + pos = m.end() + probabilities[-1] = float(m.group(1)[1:-1]) + if probabilities[-1] > 1.0: + raise ValueError( + "Production probability %f, " + "should not be greater than 1.0" % (probabilities[-1],) + ) + + # String -- add terminal. + elif line[pos] in "'\"": + m = _TERMINAL_RE.match(line, pos) + if not m: + raise ValueError("Unterminated string") + rhsides[-1].append(m.group(1)[1:-1]) + pos = m.end() + + # Vertical bar -- start new rhside. + elif line[pos] == "|": + m = _DISJUNCTION_RE.match(line, pos) + probabilities.append(0.0) + rhsides.append([]) + pos = m.end() + + # Anything else -- nonterminal. + else: + nonterm, pos = nonterm_parser(line, pos) + rhsides[-1].append(nonterm) + + if probabilistic: + return [ + ProbabilisticProduction(lhs, rhs, prob=probability) + for (rhs, probability) in zip(rhsides, probabilities) + ] + else: + return [Production(lhs, rhs) for rhs in rhsides] + + +################################################################# +# Reading Phrase Structure Grammars +################################################################# + + +def read_grammar(input, nonterm_parser, probabilistic=False, encoding=None): + """ + Return a pair consisting of a starting category and a list of + ``Productions``. + + :param input: a grammar, either in the form of a string or else + as a list of strings. + :param nonterm_parser: a function for parsing nonterminals. + It should take a ``(string, position)`` as argument and + return a ``(nonterminal, position)`` as result. + :param probabilistic: are the grammar rules probabilistic? + :type probabilistic: bool + :param encoding: the encoding of the grammar, if it is a binary string + :type encoding: str + """ + if encoding is not None: + input = input.decode(encoding) + if isinstance(input, str): + lines = input.split("\n") + else: + lines = input + + start = None + productions = [] + continue_line = "" + for linenum, line in enumerate(lines): + line = continue_line + line.strip() + if line.startswith("#") or line == "": + continue + if line.endswith("\\"): + continue_line = line[:-1].rstrip() + " " + continue + continue_line = "" + try: + if line[0] == "%": + directive, args = line[1:].split(None, 1) + if directive == "start": + start, pos = nonterm_parser(args, 0) + if pos != len(args): + raise ValueError("Bad argument to start directive") + else: + raise ValueError("Bad directive") + else: + # expand out the disjunctions on the RHS + productions += _read_production(line, nonterm_parser, probabilistic) + except ValueError as e: + raise ValueError(f"Unable to parse line {linenum + 1}: {line}\n{e}") from e + + if not productions: + raise ValueError("No productions found!") + if not start: + start = productions[0].lhs() + return (start, productions) + + +_STANDARD_NONTERM_RE = re.compile(r"( [\w/][\w/^<>-]* ) \s*", re.VERBOSE) + + +def standard_nonterm_parser(string, pos): + m = _STANDARD_NONTERM_RE.match(string, pos) + if not m: + raise ValueError("Expected a nonterminal, found: " + string[pos:]) + return (Nonterminal(m.group(1)), m.end()) + + +################################################################# +# Reading Dependency Grammars +################################################################# + +_READ_DG_RE = re.compile( + r"""^\s* # leading whitespace + ('[^']+')\s* # single-quoted lhs + (?:[-=]+>)\s* # arrow + (?:( # rhs: + "[^"]+" # doubled-quoted terminal + | '[^']+' # single-quoted terminal + | \| # disjunction + ) + \s*) # trailing space + *$""", # zero or more copies + re.VERBOSE, +) +_SPLIT_DG_RE = re.compile(r"""('[^']'|[-=]+>|"[^"]+"|'[^']+'|\|)""") + + +def _read_dependency_production(s): + if not _READ_DG_RE.match(s): + raise ValueError("Bad production string") + pieces = _SPLIT_DG_RE.split(s) + pieces = [p for i, p in enumerate(pieces) if i % 2 == 1] + lhside = pieces[0].strip("'\"") + rhsides = [[]] + for piece in pieces[2:]: + if piece == "|": + rhsides.append([]) + else: + rhsides[-1].append(piece.strip("'\"")) + return [DependencyProduction(lhside, rhside) for rhside in rhsides] + + +################################################################# +# Demonstration +################################################################# + + +def cfg_demo(): + """ + A demonstration showing how ``CFGs`` can be created and used. + """ + + from nltk import CFG, Production, nonterminals + + # Create some nonterminals + S, NP, VP, PP = nonterminals("S, NP, VP, PP") + N, V, P, Det = nonterminals("N, V, P, Det") + VP_slash_NP = VP / NP + + print("Some nonterminals:", [S, NP, VP, PP, N, V, P, Det, VP / NP]) + print(" S.symbol() =>", repr(S.symbol())) + print() + + print(Production(S, [NP])) + + # Create some Grammar Productions + grammar = CFG.fromstring( + """ + S -> NP VP + PP -> P NP + NP -> Det N | NP PP + VP -> V NP | VP PP + Det -> 'a' | 'the' + N -> 'dog' | 'cat' + V -> 'chased' | 'sat' + P -> 'on' | 'in' + """ + ) + + print("A Grammar:", repr(grammar)) + print(" grammar.start() =>", repr(grammar.start())) + print(" grammar.productions() =>", end=" ") + # Use string.replace(...) is to line-wrap the output. + print(repr(grammar.productions()).replace(",", ",\n" + " " * 25)) + print() + + +def pcfg_demo(): + """ + A demonstration showing how a ``PCFG`` can be created and used. + """ + + from nltk import induce_pcfg, treetransforms + from nltk.corpus import treebank + from nltk.parse import pchart + + toy_pcfg1 = PCFG.fromstring( + """ + S -> NP VP [1.0] + NP -> Det N [0.5] | NP PP [0.25] | 'John' [0.1] | 'I' [0.15] + Det -> 'the' [0.8] | 'my' [0.2] + N -> 'man' [0.5] | 'telescope' [0.5] + VP -> VP PP [0.1] | V NP [0.7] | V [0.2] + V -> 'ate' [0.35] | 'saw' [0.65] + PP -> P NP [1.0] + P -> 'with' [0.61] | 'under' [0.39] + """ + ) + + toy_pcfg2 = PCFG.fromstring( + """ + S -> NP VP [1.0] + VP -> V NP [.59] + VP -> V [.40] + VP -> VP PP [.01] + NP -> Det N [.41] + NP -> Name [.28] + NP -> NP PP [.31] + PP -> P NP [1.0] + V -> 'saw' [.21] + V -> 'ate' [.51] + V -> 'ran' [.28] + N -> 'boy' [.11] + N -> 'cookie' [.12] + N -> 'table' [.13] + N -> 'telescope' [.14] + N -> 'hill' [.5] + Name -> 'Jack' [.52] + Name -> 'Bob' [.48] + P -> 'with' [.61] + P -> 'under' [.39] + Det -> 'the' [.41] + Det -> 'a' [.31] + Det -> 'my' [.28] + """ + ) + + pcfg_prods = toy_pcfg1.productions() + + pcfg_prod = pcfg_prods[2] + print("A PCFG production:", repr(pcfg_prod)) + print(" pcfg_prod.lhs() =>", repr(pcfg_prod.lhs())) + print(" pcfg_prod.rhs() =>", repr(pcfg_prod.rhs())) + print(" pcfg_prod.prob() =>", repr(pcfg_prod.prob())) + print() + + grammar = toy_pcfg2 + print("A PCFG grammar:", repr(grammar)) + print(" grammar.start() =>", repr(grammar.start())) + print(" grammar.productions() =>", end=" ") + # Use .replace(...) is to line-wrap the output. + print(repr(grammar.productions()).replace(",", ",\n" + " " * 26)) + print() + + # extract productions from three trees and induce the PCFG + print("Induce PCFG grammar from treebank data:") + + productions = [] + item = treebank._fileids[0] + for tree in treebank.parsed_sents(item)[:3]: + # perform optional tree transformations, e.g.: + tree.collapse_unary(collapsePOS=False) + tree.chomsky_normal_form(horzMarkov=2) + + productions += tree.productions() + + S = Nonterminal("S") + grammar = induce_pcfg(S, productions) + print(grammar) + print() + + print("Parse sentence using induced grammar:") + + parser = pchart.InsideChartParser(grammar) + parser.trace(3) + + # doesn't work as tokens are different: + # sent = treebank.tokenized('wsj_0001.mrg')[0] + + sent = treebank.parsed_sents(item)[0].leaves() + print(sent) + for parse in parser.parse(sent): + print(parse) + + +def fcfg_demo(): + import nltk.data + + g = nltk.data.load("grammars/book_grammars/feat0.fcfg") + print(g) + print() + + +def dg_demo(): + """ + A demonstration showing the creation and inspection of a + ``DependencyGrammar``. + """ + grammar = DependencyGrammar.fromstring( + """ + 'scratch' -> 'cats' | 'walls' + 'walls' -> 'the' + 'cats' -> 'the' + """ + ) + print(grammar) + + +def sdg_demo(): + """ + A demonstration of how to read a string representation of + a CoNLL format dependency tree. + """ + from nltk.parse import DependencyGraph + + dg = DependencyGraph( + """ + 1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ + 2 had heb V V trans|ovt|1of2of3|ev 0 ROOT _ _ + 3 met met Prep Prep voor 8 mod _ _ + 4 haar haar Pron Pron bez|3|ev|neut|attr 5 det _ _ + 5 moeder moeder N N soort|ev|neut 3 obj1 _ _ + 6 kunnen kan V V hulp|ott|1of2of3|mv 2 vc _ _ + 7 gaan ga V V hulp|inf 6 vc _ _ + 8 winkelen winkel V V intrans|inf 11 cnj _ _ + 9 , , Punc Punc komma 8 punct _ _ + 10 zwemmen zwem V V intrans|inf 11 cnj _ _ + 11 of of Conj Conj neven 7 vc _ _ + 12 terrassen terras N N soort|mv|neut 11 cnj _ _ + 13 . . Punc Punc punt 12 punct _ _ + """ + ) + tree = dg.tree() + print(tree.pprint()) + + +def demo(): + cfg_demo() + pcfg_demo() + fcfg_demo() + dg_demo() + sdg_demo() + + +if __name__ == "__main__": + demo() + +__all__ = [ + "Nonterminal", + "nonterminals", + "CFG", + "Production", + "PCFG", + "ProbabilisticProduction", + "DependencyGrammar", + "DependencyProduction", + "ProbabilisticDependencyGrammar", + "induce_pcfg", + "read_grammar", +] diff --git a/nltk/help.py b/nltk/help.py new file mode 100644 index 0000000..c5254e7 --- /dev/null +++ b/nltk/help.py @@ -0,0 +1,68 @@ +# Natural Language Toolkit (NLTK) Help +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Provide structured access to documentation. +""" + +import json +import re +from textwrap import wrap + +from nltk.data import find, open_datafile + + +def brown_tagset(tagpattern=None): + _format_tagset("brown_tagset", tagpattern) + + +def claws5_tagset(tagpattern=None): + _format_tagset("claws5_tagset", tagpattern) + + +def upenn_tagset(tagpattern=None): + _format_tagset("upenn_tagset", tagpattern) + + +##################################################################### +# UTILITIES +##################################################################### + + +def _print_entries(tags, tagdict): + for tag in tags: + entry = tagdict[tag] + defn = [tag + ": " + entry[0]] + examples = wrap( + entry[1], width=75, initial_indent=" ", subsequent_indent=" " + ) + print("\n".join(defn + examples)) + + +def _format_tagset(tagset, tagpattern=None): + # Load tagset from json file. + with open_datafile(find("help/tagsets_json/PY3_json/"), f"{tagset}.json") as fin: + tagdict = json.load(fin) + + if not tagpattern: + _print_entries(sorted(tagdict), tagdict) + elif tagpattern in tagdict: + _print_entries([tagpattern], tagdict) + else: + tagpattern = re.compile(tagpattern) + tags = [tag for tag in sorted(tagdict) if tagpattern.match(tag)] + if tags: + _print_entries(tags, tagdict) + else: + print("No matching tags found.") + + +if __name__ == "__main__": + brown_tagset(r"NN.*") + upenn_tagset(r".*\$") + claws5_tagset("UNDEFINED") + brown_tagset(r"NN") diff --git a/nltk/huggingface/__init__.py b/nltk/huggingface/__init__.py new file mode 100644 index 0000000..83911d6 --- /dev/null +++ b/nltk/huggingface/__init__.py @@ -0,0 +1,9 @@ +# Natural Language Toolkit: HuggingFace dataset integration +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +from nltk.huggingface.dataset import download + +__all__ = ["download"] diff --git a/nltk/huggingface/dataset.py b/nltk/huggingface/dataset.py new file mode 100644 index 0000000..229a8e6 --- /dev/null +++ b/nltk/huggingface/dataset.py @@ -0,0 +1,334 @@ +# Natural Language Toolkit: HuggingFace dataset integration +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +HuggingFace datasets integration for NLTK. + +Provides a PathPointer subclass that reads directly from the HuggingFace +datasets cache, and a ``download()`` function that populates that cache. + +Usage:: + + import nltk + nltk.download('stopwords', hf=True) # download to HF cache + nltk.corpus.stopwords.words('portuguese') # HF fallback if not in ~/nltk_data + nltk.corpus.stopwords.words('portuguese', hf=True) # HF cache directly + +Registry schema +--------------- +Each entry in ``REGISTRY`` must declare: + +``repo`` (str) + HuggingFace dataset repo id, e.g. ``"nltk-data-hub/stopwords"``. + +``split`` (str) + HF split name to load, e.g. ``"stopwords"``, ``"train"``. + +``structure`` (str) + How the corpus is organised on HF: + + ``"multi_config"`` + One HF config per NLTK fileid. No assumption about what that + dimension represents (language, category, author, etc.). + ``fileid`` → config name. + + ``"flat"`` + Single config, flat table. A ``fileid_column`` value is used + to select rows for a given fileid. + + ``"single"`` + Single config, no sub-selection. The whole split is the corpus. + +``content_type`` (str) + How rows are serialised to the byte/text stream NLTK readers expect: + + ``"word_list"`` + Each row is one entry; ``text_column`` holds the string. + Serialised as one entry per line. + + ``"raw_text"`` + Rows have a ``text_column`` with full document text. When a + fileid is given, rows are filtered by ``fileid_column``. + Serialised as the raw text string. + + (Add new types here as more corpora are onboarded.) + +``cache_probe`` (str) + A single parquet path inside the repo used to detect whether the + corpus has already been downloaded locally. No network request is + made; ``huggingface_hub.try_to_load_from_cache`` inspects the local + filesystem only. + +Optional keys (required by certain content types): + +``text_column`` column that holds the main text / word value. +``fileid_column`` column that identifies which NLTK fileid a row belongs to. +``label_column`` column for classification labels (future use). +""" + +import io + +# --------------------------------------------------------------------------- +# Registry +# --------------------------------------------------------------------------- + +REGISTRY = { + "stopwords": { + "repo": "nltk-data-hub/stopwords", + "split": "stopwords", + "structure": "multi_config", + "content_type": "word_list", + "text_column": "word", + "cache_probe": "data/english/stopwords.parquet", + }, +} + + +# --------------------------------------------------------------------------- +# Cache detection (no network) +# --------------------------------------------------------------------------- + + +def _is_cached(corpus_id): + """Return True if the corpus parquet exists in the local HF datasets cache.""" + info = REGISTRY.get(corpus_id) + if info is None: + return False + try: + from huggingface_hub import try_to_load_from_cache + + result = try_to_load_from_cache( + repo_id=info["repo"], + filename=info["cache_probe"], + repo_type="dataset", + ) + return result is not None and result != "no_connection" + except Exception: + return False + + +# --------------------------------------------------------------------------- +# Content serialisation +# --------------------------------------------------------------------------- + + +def _serialise(ds, info, fileid=None): + """ + Convert an HF dataset ``ds`` to the byte/text content that an NLTK + corpus reader would find in a plain file. + + :param ds: ``datasets.Dataset`` already filtered/selected for this corpus. + :param info: REGISTRY entry for the corpus. + :param fileid: the NLTK fileid being requested (used by some types). + :returns: str — file content as NLTK expects it. + """ + content_type = info.get("content_type", "raw_text") + + if content_type == "word_list": + return "\n".join(ds[info["text_column"]]) + + if content_type == "raw_text": + col = info["text_column"] + texts = ds[col] + return "\n".join(texts) if len(texts) > 1 else (texts[0] if texts else "") + + raise NotImplementedError( + f"content_type={content_type!r} is not implemented. " + "Add a handler in nltk.huggingface.dataset._serialise()." + ) + + +def _load_hf_dataset(info, fileid=None): + """ + Load the appropriate HF dataset slice for *fileid*, respecting structure. + + :param info: REGISTRY entry. + :param fileid: NLTK fileid (may be None for single-structure corpora). + :returns: ``datasets.Dataset``. + """ + from datasets import load_dataset + + structure = info.get("structure", "single") + + if structure == "multi_config": + if fileid is None: + raise ValueError( + "fileid is required for multi_config corpora. " + "Pass the config name (e.g. a language or category)." + ) + return load_dataset(info["repo"], fileid, split=info["split"]) + + if structure == "flat": + ds = load_dataset(info["repo"], split=info["split"]) + if fileid is not None: + col = info["fileid_column"] + ds = ds.filter(lambda row: row[col] == fileid) + return ds + + # single + return load_dataset(info["repo"], split=info["split"]) + + +# --------------------------------------------------------------------------- +# HFDatasetPathPointer +# --------------------------------------------------------------------------- + + +class HFDatasetPathPointer: + """ + A ``PathPointer``-compatible object backed by a HuggingFace dataset + stored in the local HF datasets cache (~/.cache/huggingface/datasets/). + + Satisfies the NLTK PathPointer interface (``open`` / ``file_size`` / + ``join``) so that existing corpus readers work unchanged after + ``nltk.download(..., hf=True)``. + """ + + def __init__(self, corpus_id, fileid=None): + self.corpus_id = corpus_id + self.fileid = fileid + + # -- PathPointer interface ----------------------------------------------- + + def open(self, encoding=None): + """Return a stream of file content as NLTK corpus readers expect.""" + info = REGISTRY[self.corpus_id] + ds = _load_hf_dataset(info, fileid=self.fileid) + content = _serialise(ds, info, fileid=self.fileid) + if encoding: + return io.StringIO(content) + return io.BytesIO(content.encode("utf-8")) + + def file_size(self): + return 0 + + def join(self, fileid): + return HFDatasetPathPointer(self.corpus_id, fileid) + + # -- fileids (duck-typed by find_corpus_fileids) ------------------------- + + def fileids(self): + """Return sorted list of fileids available for this corpus.""" + info = REGISTRY.get(self.corpus_id) + if info is None or not _is_cached(self.corpus_id): + return [] + structure = info.get("structure", "single") + try: + if structure == "multi_config": + from datasets import get_dataset_config_names + + return sorted(get_dataset_config_names(info["repo"])) + if structure == "flat": + from datasets import load_dataset + + ds = load_dataset(info["repo"], split=info["split"]) + return sorted(ds.unique(info["fileid_column"])) + return [info["split"]] # single + except Exception: + return [] + + # -- repr / path --------------------------------------------------------- + + @property + def path(self): + repo = REGISTRY.get(self.corpus_id, {}).get("repo", self.corpus_id) + return f"hf://{repo}" + + def __str__(self): + return f"{self.path}/{self.fileid}" if self.fileid else self.path + + def __repr__(self): + return f"HFDatasetPathPointer({self.corpus_id!r}, {self.fileid!r})" + + +# Register as virtual subclass of PathPointer — avoids circular import at load +def _register_path_pointer(): + try: + from nltk.data import PathPointer + + PathPointer.register(HFDatasetPathPointer) + except Exception: + pass + + +_register_path_pointer() + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def download(corpus_id, token=None, quiet=False): + """ + Download an NLTK corpus from HuggingFace into the HF datasets cache + (``~/.cache/huggingface/datasets/``). + + :param corpus_id: NLTK corpus id, e.g. ``'stopwords'``. + :param token: optional HuggingFace API token for private repos. + :param quiet: suppress progress output. + :raises ValueError: if *corpus_id* is not in the HF registry. + """ + info = REGISTRY.get(corpus_id) + if info is None: + raise ValueError( + f"Corpus {corpus_id!r} is not available on HuggingFace.\n" + f"Available: {sorted(REGISTRY)}" + ) + + from datasets import load_dataset + + kwargs = {"token": token} if token else {} + structure = info.get("structure", "single") + + if structure == "multi_config": + from datasets import get_dataset_config_names + + configs = get_dataset_config_names(info["repo"]) + result = { + cfg: load_dataset(info["repo"], cfg, split=info["split"], **kwargs) + for cfg in configs + } + if not quiet: + total = sum(len(d) for d in result.values()) + print( + f"[nltk_hf] '{corpus_id}' downloaded from {info['repo']} " + f"({len(configs)} configs, {total:,} rows)" + ) + return result + + else: # flat or single + ds = load_dataset(info["repo"], split=info["split"], **kwargs) + if not quiet: + print( + f"[nltk_hf] '{corpus_id}' downloaded from {info['repo']} " + f"({len(ds):,} rows)" + ) + return ds + + +def load_data(corpus_id, fileid=None): + """ + Load data for *corpus_id* directly from the HF datasets cache and return + it as a string in the format NLTK corpus readers expect. + + :param corpus_id: NLTK corpus id, e.g. ``'stopwords'``. + :param fileid: sub-resource identifier (config name, category, fileid, …). + :returns: str. + :raises LookupError: if the corpus is not in the registry or not cached. + """ + info = REGISTRY.get(corpus_id) + if info is None: + raise LookupError( + f"Corpus {corpus_id!r} is not in the HuggingFace NLTK registry." + ) + if not _is_cached(corpus_id): + raise LookupError( + f"Corpus {corpus_id!r} not found in HF datasets cache. " + f"Run: nltk.download({corpus_id!r}, hf=True)" + ) + ds = _load_hf_dataset(info, fileid=fileid) + return _serialise(ds, info, fileid=fileid) diff --git a/nltk/inference/__init__.py b/nltk/inference/__init__.py new file mode 100644 index 0000000..e887847 --- /dev/null +++ b/nltk/inference/__init__.py @@ -0,0 +1,24 @@ +# Natural Language Toolkit: Inference +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Dan Garrette +# Ewan Klein +# +# URL: +# For license information, see LICENSE.TXT + +""" +Classes and interfaces for theorem proving and model building. +""" + +from nltk.inference.api import ParallelProverBuilder, ParallelProverBuilderCommand +from nltk.inference.discourse import ( + CfgReadingCommand, + DiscourseTester, + DrtGlueReadingCommand, + ReadingCommand, +) +from nltk.inference.mace import Mace, MaceCommand +from nltk.inference.prover9 import Prover9, Prover9Command +from nltk.inference.resolution import ResolutionProver, ResolutionProverCommand +from nltk.inference.tableau import TableauProver, TableauProverCommand diff --git a/nltk/inference/api.py b/nltk/inference/api.py new file mode 100644 index 0000000..93c513f --- /dev/null +++ b/nltk/inference/api.py @@ -0,0 +1,614 @@ +# Natural Language Toolkit: Classifier Interface +# +# Author: Ewan Klein +# Dan Garrette +# +# URL: +# For license information, see LICENSE.TXT + +""" +Interfaces and base classes for theorem provers and model builders. + +``Prover`` is a standard interface for a theorem prover which tries to prove a goal from a +list of assumptions. + +``ModelBuilder`` is a standard interface for a model builder. Given just a set of assumptions. +the model builder tries to build a model for the assumptions. Given a set of assumptions and a +goal *G*, the model builder tries to find a counter-model, in the sense of a model that will satisfy +the assumptions plus the negation of *G*. +""" + +import threading +import time +from abc import ABCMeta, abstractmethod + + +class Prover(metaclass=ABCMeta): + """ + Interface for trying to prove a goal from assumptions. Both the goal and + the assumptions are constrained to be formulas of ``logic.Expression``. + """ + + def prove(self, goal=None, assumptions=None, verbose=False): + """ + :return: Whether the proof was successful or not. + :rtype: bool + """ + return self._prove(goal, assumptions, verbose)[0] + + @abstractmethod + def _prove(self, goal=None, assumptions=None, verbose=False): + """ + :return: Whether the proof was successful or not, along with the proof + :rtype: tuple: (bool, str) + """ + + +class ModelBuilder(metaclass=ABCMeta): + """ + Interface for trying to build a model of set of formulas. + Open formulas are assumed to be universally quantified. + Both the goal and the assumptions are constrained to be formulas + of ``logic.Expression``. + """ + + def build_model(self, goal=None, assumptions=None, verbose=False): + """ + Perform the actual model building. + :return: Whether a model was generated + :rtype: bool + """ + return self._build_model(goal, assumptions, verbose)[0] + + @abstractmethod + def _build_model(self, goal=None, assumptions=None, verbose=False): + """ + Perform the actual model building. + :return: Whether a model was generated, and the model itself + :rtype: tuple(bool, sem.Valuation) + """ + + +class TheoremToolCommand(metaclass=ABCMeta): + """ + This class holds a goal and a list of assumptions to be used in proving + or model building. + """ + + @abstractmethod + def add_assumptions(self, new_assumptions): + """ + Add new assumptions to the assumption list. + + :param new_assumptions: new assumptions + :type new_assumptions: list(sem.Expression) + """ + + @abstractmethod + def retract_assumptions(self, retracted, debug=False): + """ + Retract assumptions from the assumption list. + + :param debug: If True, give warning when ``retracted`` is not present on + assumptions list. + :type debug: bool + :param retracted: assumptions to be retracted + :type retracted: list(sem.Expression) + """ + + @abstractmethod + def assumptions(self): + """ + List the current assumptions. + + :return: list of ``Expression`` + """ + + @abstractmethod + def goal(self): + """ + Return the goal + + :return: ``Expression`` + """ + + @abstractmethod + def print_assumptions(self): + """ + Print the list of the current assumptions. + """ + + +class ProverCommand(TheoremToolCommand): + """ + This class holds a ``Prover``, a goal, and a list of assumptions. When + prove() is called, the ``Prover`` is executed with the goal and assumptions. + """ + + @abstractmethod + def prove(self, verbose=False): + """ + Perform the actual proof. + """ + + @abstractmethod + def proof(self, simplify=True): + """ + Return the proof string + :param simplify: bool simplify the proof? + :return: str + """ + + @abstractmethod + def get_prover(self): + """ + Return the prover object + :return: ``Prover`` + """ + + +class ModelBuilderCommand(TheoremToolCommand): + """ + This class holds a ``ModelBuilder``, a goal, and a list of assumptions. + When build_model() is called, the ``ModelBuilder`` is executed with the goal + and assumptions. + """ + + @abstractmethod + def build_model(self, verbose=False): + """ + Perform the actual model building. + :return: A model if one is generated; None otherwise. + :rtype: sem.Valuation + """ + + @abstractmethod + def model(self, format=None): + """ + Return a string representation of the model + + :param simplify: bool simplify the proof? + :return: str + """ + + @abstractmethod + def get_model_builder(self): + """ + Return the model builder object + :return: ``ModelBuilder`` + """ + + +class BaseTheoremToolCommand(TheoremToolCommand): + """ + This class holds a goal and a list of assumptions to be used in proving + or model building. + """ + + def __init__(self, goal=None, assumptions=None): + """ + :param goal: Input expression to prove + :type goal: sem.Expression + :param assumptions: Input expressions to use as assumptions in + the proof. + :type assumptions: list(sem.Expression) + """ + self._goal = goal + + if not assumptions: + self._assumptions = [] + else: + self._assumptions = list(assumptions) + + self._result = None + """A holder for the result, to prevent unnecessary re-proving""" + + def add_assumptions(self, new_assumptions): + """ + Add new assumptions to the assumption list. + + :param new_assumptions: new assumptions + :type new_assumptions: list(sem.Expression) + """ + self._assumptions.extend(new_assumptions) + self._result = None + + def retract_assumptions(self, retracted, debug=False): + """ + Retract assumptions from the assumption list. + + :param debug: If True, give warning when ``retracted`` is not present on + assumptions list. + :type debug: bool + :param retracted: assumptions to be retracted + :type retracted: list(sem.Expression) + """ + retracted = set(retracted) + result_list = list(filter(lambda a: a not in retracted, self._assumptions)) + if debug and result_list == self._assumptions: + print(Warning("Assumptions list has not been changed:")) + self.print_assumptions() + + self._assumptions = result_list + + self._result = None + + def assumptions(self): + """ + List the current assumptions. + + :return: list of ``Expression`` + """ + return self._assumptions + + def goal(self): + """ + Return the goal + + :return: ``Expression`` + """ + return self._goal + + def print_assumptions(self): + """ + Print the list of the current assumptions. + """ + for a in self.assumptions(): + print(a) + + +class BaseProverCommand(BaseTheoremToolCommand, ProverCommand): + """ + This class holds a ``Prover``, a goal, and a list of assumptions. When + prove() is called, the ``Prover`` is executed with the goal and assumptions. + """ + + def __init__(self, prover, goal=None, assumptions=None): + """ + :param prover: The theorem tool to execute with the assumptions + :type prover: Prover + :see: ``BaseTheoremToolCommand`` + """ + self._prover = prover + """The theorem tool to execute with the assumptions""" + + BaseTheoremToolCommand.__init__(self, goal, assumptions) + + self._proof = None + + def prove(self, verbose=False): + """ + Perform the actual proof. Store the result to prevent unnecessary + re-proving. + """ + if self._result is None: + self._result, self._proof = self._prover._prove( + self.goal(), self.assumptions(), verbose + ) + return self._result + + def proof(self, simplify=True): + """ + Return the proof string + :param simplify: bool simplify the proof? + :return: str + """ + if self._result is None: + raise LookupError("You have to call prove() first to get a proof!") + else: + return self.decorate_proof(self._proof, simplify) + + def decorate_proof(self, proof_string, simplify=True): + """ + Modify and return the proof string + :param proof_string: str the proof to decorate + :param simplify: bool simplify the proof? + :return: str + """ + return proof_string + + def get_prover(self): + return self._prover + + +class BaseModelBuilderCommand(BaseTheoremToolCommand, ModelBuilderCommand): + """ + This class holds a ``ModelBuilder``, a goal, and a list of assumptions. When + build_model() is called, the ``ModelBuilder`` is executed with the goal and + assumptions. + """ + + def __init__(self, modelbuilder, goal=None, assumptions=None): + """ + :param modelbuilder: The theorem tool to execute with the assumptions + :type modelbuilder: ModelBuilder + :see: ``BaseTheoremToolCommand`` + """ + self._modelbuilder = modelbuilder + """The theorem tool to execute with the assumptions""" + + BaseTheoremToolCommand.__init__(self, goal, assumptions) + + self._model = None + + def build_model(self, verbose=False): + """ + Attempt to build a model. Store the result to prevent unnecessary + re-building. + """ + if self._result is None: + self._result, self._model = self._modelbuilder._build_model( + self.goal(), self.assumptions(), verbose + ) + return self._result + + def model(self, format=None): + """ + Return a string representation of the model + + :param simplify: bool simplify the proof? + :return: str + """ + if self._result is None: + raise LookupError("You have to call build_model() first to " "get a model!") + else: + return self._decorate_model(self._model, format) + + def _decorate_model(self, valuation_str, format=None): + """ + :param valuation_str: str with the model builder's output + :param format: str indicating the format for displaying + :return: str + """ + return valuation_str + + def get_model_builder(self): + return self._modelbuilder + + +class TheoremToolCommandDecorator(TheoremToolCommand): + """ + A base decorator for the ``ProverCommandDecorator`` and + ``ModelBuilderCommandDecorator`` classes from which decorators can extend. + """ + + def __init__(self, command): + """ + :param command: ``TheoremToolCommand`` to decorate + """ + self._command = command + + # The decorator has its own versions of 'result' different from the + # underlying command + self._result = None + + def assumptions(self): + return self._command.assumptions() + + def goal(self): + return self._command.goal() + + def add_assumptions(self, new_assumptions): + self._command.add_assumptions(new_assumptions) + self._result = None + + def retract_assumptions(self, retracted, debug=False): + self._command.retract_assumptions(retracted, debug) + self._result = None + + def print_assumptions(self): + self._command.print_assumptions() + + +class ProverCommandDecorator(TheoremToolCommandDecorator, ProverCommand): + """ + A base decorator for the ``ProverCommand`` class from which other + prover command decorators can extend. + """ + + def __init__(self, proverCommand): + """ + :param proverCommand: ``ProverCommand`` to decorate + """ + TheoremToolCommandDecorator.__init__(self, proverCommand) + + # The decorator has its own versions of 'result' and 'proof' + # because they may be different from the underlying command + self._proof = None + + def prove(self, verbose=False): + if self._result is None: + prover = self.get_prover() + self._result, self._proof = prover._prove( + self.goal(), self.assumptions(), verbose + ) + return self._result + + def proof(self, simplify=True): + """ + Return the proof string + :param simplify: bool simplify the proof? + :return: str + """ + if self._result is None: + raise LookupError("You have to call prove() first to get a proof!") + else: + return self.decorate_proof(self._proof, simplify) + + def decorate_proof(self, proof_string, simplify=True): + """ + Modify and return the proof string + :param proof_string: str the proof to decorate + :param simplify: bool simplify the proof? + :return: str + """ + return self._command.decorate_proof(proof_string, simplify) + + def get_prover(self): + return self._command.get_prover() + + +class ModelBuilderCommandDecorator(TheoremToolCommandDecorator, ModelBuilderCommand): + """ + A base decorator for the ``ModelBuilderCommand`` class from which other + prover command decorators can extend. + """ + + def __init__(self, modelBuilderCommand): + """ + :param modelBuilderCommand: ``ModelBuilderCommand`` to decorate + """ + TheoremToolCommandDecorator.__init__(self, modelBuilderCommand) + + # The decorator has its own versions of 'result' and 'valuation' + # because they may be different from the underlying command + self._model = None + + def build_model(self, verbose=False): + """ + Attempt to build a model. Store the result to prevent unnecessary + re-building. + """ + if self._result is None: + modelbuilder = self.get_model_builder() + self._result, self._model = modelbuilder._build_model( + self.goal(), self.assumptions(), verbose + ) + return self._result + + def model(self, format=None): + """ + Return a string representation of the model + + :param simplify: bool simplify the proof? + :return: str + """ + if self._result is None: + raise LookupError("You have to call build_model() first to " "get a model!") + else: + return self._decorate_model(self._model, format) + + def _decorate_model(self, valuation_str, format=None): + """ + Modify and return the proof string + :param valuation_str: str with the model builder's output + :param format: str indicating the format for displaying + :return: str + """ + return self._command._decorate_model(valuation_str, format) + + def get_model_builder(self): + return self._command.get_prover() + + +class ParallelProverBuilder(Prover, ModelBuilder): + """ + This class stores both a prover and a model builder and when either + prove() or build_model() is called, then both theorem tools are run in + parallel. Whichever finishes first, the prover or the model builder, is the + result that will be used. + """ + + def __init__(self, prover, modelbuilder): + self._prover = prover + self._modelbuilder = modelbuilder + + def _prove(self, goal=None, assumptions=None, verbose=False): + return self._run(goal, assumptions, verbose), "" + + def _build_model(self, goal=None, assumptions=None, verbose=False): + return not self._run(goal, assumptions, verbose), "" + + def _run(self, goal, assumptions, verbose): + # Set up two thread, Prover and ModelBuilder to run in parallel + tp_thread = TheoremToolThread( + lambda: self._prover.prove(goal, assumptions, verbose), verbose, "TP" + ) + mb_thread = TheoremToolThread( + lambda: self._modelbuilder.build_model(goal, assumptions, verbose), + verbose, + "MB", + ) + + tp_thread.start() + mb_thread.start() + + while tp_thread.is_alive() and mb_thread.is_alive(): + # wait until either the prover or the model builder is done + pass + + if tp_thread.result is not None: + return tp_thread.result + elif mb_thread.result is not None: + return not mb_thread.result + else: + return None + + +class ParallelProverBuilderCommand(BaseProverCommand, BaseModelBuilderCommand): + """ + This command stores both a prover and a model builder and when either + prove() or build_model() is called, then both theorem tools are run in + parallel. Whichever finishes first, the prover or the model builder, is the + result that will be used. + + Because the theorem prover result is the opposite of the model builder + result, we will treat self._result as meaning "proof found/no model found". + """ + + def __init__(self, prover, modelbuilder, goal=None, assumptions=None): + BaseProverCommand.__init__(self, prover, goal, assumptions) + BaseModelBuilderCommand.__init__(self, modelbuilder, goal, assumptions) + + def prove(self, verbose=False): + return self._run(verbose) + + def build_model(self, verbose=False): + return not self._run(verbose) + + def _run(self, verbose): + # Set up two thread, Prover and ModelBuilder to run in parallel + tp_thread = TheoremToolThread( + lambda: BaseProverCommand.prove(self, verbose), verbose, "TP" + ) + mb_thread = TheoremToolThread( + lambda: BaseModelBuilderCommand.build_model(self, verbose), verbose, "MB" + ) + + tp_thread.start() + mb_thread.start() + + while tp_thread.is_alive() and mb_thread.is_alive(): + # wait until either the prover or the model builder is done + pass + + if tp_thread.result is not None: + self._result = tp_thread.result + elif mb_thread.result is not None: + self._result = not mb_thread.result + return self._result + + +class TheoremToolThread(threading.Thread): + def __init__(self, command, verbose, name=None): + threading.Thread.__init__(self) + self._command = command + self._result = None + self._verbose = verbose + self._name = name + + def run(self): + try: + self._result = self._command() + if self._verbose: + print( + "Thread %s finished with result %s at %s" + % (self._name, self._result, time.localtime(time.time())) + ) + except Exception as e: + print(e) + print("Thread %s completed abnormally" % (self._name)) + + @property + def result(self): + return self._result diff --git a/nltk/inference/discourse.py b/nltk/inference/discourse.py new file mode 100644 index 0000000..f933ee7 --- /dev/null +++ b/nltk/inference/discourse.py @@ -0,0 +1,651 @@ +# Natural Language Toolkit: Discourse Processing +# +# Author: Ewan Klein +# Dan Garrette +# +# URL: +# For license information, see LICENSE.TXT + +r""" +Module for incrementally developing simple discourses, and checking for semantic ambiguity, +consistency and informativeness. + +Many of the ideas are based on the CURT family of programs of Blackburn and Bos +(see http://homepages.inf.ed.ac.uk/jbos/comsem/book1.html). + +Consistency checking is carried out by using the ``mace`` module to call the Mace4 model builder. +Informativeness checking is carried out with a call to ``Prover.prove()`` from +the ``inference`` module. + +``DiscourseTester`` is a constructor for discourses. +The basic data structure is a list of sentences, stored as ``self._sentences``. Each sentence in the list +is assigned a "sentence ID" (``sid``) of the form ``s``\ *i*. For example:: + + s0: A boxer walks + s1: Every boxer chases a girl + +Each sentence can be ambiguous between a number of readings, each of which receives a +"reading ID" (``rid``) of the form ``s``\ *i* -``r``\ *j*. For example:: + + s0 readings: + + s0-r1: some x.(boxer(x) & walk(x)) + s0-r0: some x.(boxerdog(x) & walk(x)) + +A "thread" is a list of readings, represented as a list of ``rid``\ s. +Each thread receives a "thread ID" (``tid``) of the form ``d``\ *i*. +For example:: + + d0: ['s0-r0', 's1-r0'] + +The set of all threads for a discourse is the Cartesian product of all the readings of the sequences of sentences. +(This is not intended to scale beyond very short discourses!) The method ``readings(filter=True)`` will only show +those threads which are consistent (taking into account any background assumptions). +""" + +import os +from abc import ABCMeta, abstractmethod +from functools import reduce +from operator import add, and_ + +from nltk.data import show_cfg +from nltk.inference.mace import MaceCommand +from nltk.inference.prover9 import Prover9Command +from nltk.parse import load_parser +from nltk.parse.malt import MaltParser +from nltk.sem.drt import AnaphoraResolutionException, resolve_anaphora +from nltk.sem.glue import DrtGlue +from nltk.sem.logic import Expression +from nltk.tag import RegexpTagger + + +class ReadingCommand(metaclass=ABCMeta): + @abstractmethod + def parse_to_readings(self, sentence): + """ + :param sentence: the sentence to read + :type sentence: str + """ + + def process_thread(self, sentence_readings): + """ + This method should be used to handle dependencies between readings such + as resolving anaphora. + + :param sentence_readings: readings to process + :type sentence_readings: list(Expression) + :return: the list of readings after processing + :rtype: list(Expression) + """ + return sentence_readings + + @abstractmethod + def combine_readings(self, readings): + """ + :param readings: readings to combine + :type readings: list(Expression) + :return: one combined reading + :rtype: Expression + """ + + @abstractmethod + def to_fol(self, expression): + """ + Convert this expression into a First-Order Logic expression. + + :param expression: an expression + :type expression: Expression + :return: a FOL version of the input expression + :rtype: Expression + """ + + +class CfgReadingCommand(ReadingCommand): + def __init__(self, gramfile=None): + """ + :param gramfile: name of file where grammar can be loaded + :type gramfile: str + """ + self._gramfile = ( + gramfile if gramfile else "grammars/book_grammars/discourse.fcfg" + ) + self._parser = load_parser(self._gramfile) + + def parse_to_readings(self, sentence): + """:see: ReadingCommand.parse_to_readings()""" + from nltk.sem import root_semrep + + tokens = sentence.split() + trees = self._parser.parse(tokens) + return [root_semrep(tree) for tree in trees] + + def combine_readings(self, readings): + """:see: ReadingCommand.combine_readings()""" + return reduce(and_, readings) + + def to_fol(self, expression): + """:see: ReadingCommand.to_fol()""" + return expression + + +class DrtGlueReadingCommand(ReadingCommand): + def __init__(self, semtype_file=None, remove_duplicates=False, depparser=None): + """ + :param semtype_file: name of file where grammar can be loaded + :param remove_duplicates: should duplicates be removed? + :param depparser: the dependency parser + """ + if semtype_file is None: + semtype_file = os.path.join( + "grammars", "sample_grammars", "drt_glue.semtype" + ) + self._glue = DrtGlue( + semtype_file=semtype_file, + remove_duplicates=remove_duplicates, + depparser=depparser, + ) + + def parse_to_readings(self, sentence): + """:see: ReadingCommand.parse_to_readings()""" + return self._glue.parse_to_meaning(sentence) + + def process_thread(self, sentence_readings): + """:see: ReadingCommand.process_thread()""" + try: + return [self.combine_readings(sentence_readings)] + except AnaphoraResolutionException: + return [] + + def combine_readings(self, readings): + """:see: ReadingCommand.combine_readings()""" + thread_reading = reduce(add, readings) + return resolve_anaphora(thread_reading.simplify()) + + def to_fol(self, expression): + """:see: ReadingCommand.to_fol()""" + return expression.fol() + + +class DiscourseTester: + """ + Check properties of an ongoing discourse. + """ + + def __init__(self, input, reading_command=None, background=None): + """ + Initialize a ``DiscourseTester``. + + :param input: the discourse sentences + :type input: list of str + :param background: Formulas which express background assumptions + :type background: list(Expression) + """ + self._input = input + self._sentences = {"s%s" % i: sent for i, sent in enumerate(input)} + self._models = None + self._readings = {} + self._reading_command = ( + reading_command if reading_command else CfgReadingCommand() + ) + self._threads = {} + self._filtered_threads = {} + if background is not None: + from nltk.sem.logic import Expression + + for e in background: + assert isinstance(e, Expression) + self._background = background + else: + self._background = [] + + ############################### + # Sentences + ############################### + + def sentences(self): + """ + Display the list of sentences in the current discourse. + """ + for id in sorted(self._sentences): + print(f"{id}: {self._sentences[id]}") + + def add_sentence(self, sentence, informchk=False, consistchk=False): + """ + Add a sentence to the current discourse. + + Updates ``self._input`` and ``self._sentences``. + :param sentence: An input sentence + :type sentence: str + :param informchk: if ``True``, check that the result of adding the sentence is thread-informative. Updates ``self._readings``. + :param consistchk: if ``True``, check that the result of adding the sentence is thread-consistent. Updates ``self._readings``. + + """ + # check whether the new sentence is informative (i.e. not entailed by the previous discourse) + if informchk: + self.readings(verbose=False) + for tid in sorted(self._threads): + assumptions = [reading for (rid, reading) in self.expand_threads(tid)] + assumptions += self._background + for sent_reading in self._get_readings(sentence): + tp = Prover9Command(goal=sent_reading, assumptions=assumptions) + if tp.prove(): + print( + "Sentence '%s' under reading '%s':" + % (sentence, str(sent_reading)) + ) + print("Not informative relative to thread '%s'" % tid) + + self._input.append(sentence) + self._sentences = {"s%s" % i: sent for i, sent in enumerate(self._input)} + # check whether adding the new sentence to the discourse preserves consistency (i.e. a model can be found for the combined set of + # of assumptions + if consistchk: + self.readings(verbose=False) + self.models(show=False) + + def retract_sentence(self, sentence, verbose=True): + """ + Remove a sentence from the current discourse. + + Updates ``self._input``, ``self._sentences`` and ``self._readings``. + :param sentence: An input sentence + :type sentence: str + :param verbose: If ``True``, report on the updated list of sentences. + """ + try: + self._input.remove(sentence) + except ValueError: + print( + "Retraction failed. The sentence '%s' is not part of the current discourse:" + % sentence + ) + self.sentences() + return None + self._sentences = {"s%s" % i: sent for i, sent in enumerate(self._input)} + self.readings(verbose=False) + if verbose: + print("Current sentences are ") + self.sentences() + + def grammar(self): + """ + Print out the grammar in use for parsing input sentences + """ + show_cfg(self._reading_command._gramfile) + + ############################### + # Readings and Threads + ############################### + + def _get_readings(self, sentence): + """ + Build a list of semantic readings for a sentence. + + :rtype: list(Expression) + """ + return self._reading_command.parse_to_readings(sentence) + + def _construct_readings(self): + """ + Use ``self._sentences`` to construct a value for ``self._readings``. + """ + # re-initialize self._readings in case we have retracted a sentence + self._readings = {} + for sid in sorted(self._sentences): + sentence = self._sentences[sid] + readings = self._get_readings(sentence) + self._readings[sid] = { + f"{sid}-r{rid}": reading.simplify() + for rid, reading in enumerate(sorted(readings, key=str)) + } + + def _construct_threads(self): + """ + Use ``self._readings`` to construct a value for ``self._threads`` + and use the model builder to construct a value for ``self._filtered_threads`` + """ + thread_list = [[]] + for sid in sorted(self._readings): + thread_list = self.multiply(thread_list, sorted(self._readings[sid])) + self._threads = {"d%s" % tid: thread for tid, thread in enumerate(thread_list)} + # re-initialize the filtered threads + self._filtered_threads = {} + # keep the same ids, but only include threads which get models + consistency_checked = self._check_consistency(self._threads) + for tid, thread in self._threads.items(): + if (tid, True) in consistency_checked: + self._filtered_threads[tid] = thread + + def _show_readings(self, sentence=None): + """ + Print out the readings for the discourse (or a single sentence). + """ + if sentence is not None: + print("The sentence '%s' has these readings:" % sentence) + for r in [str(reading) for reading in (self._get_readings(sentence))]: + print(" %s" % r) + else: + for sid in sorted(self._readings): + print() + print("%s readings:" % sid) + print() #'-' * 30 + for rid in sorted(self._readings[sid]): + lf = self._readings[sid][rid] + print(f"{rid}: {lf.normalize()}") + + def _show_threads(self, filter=False, show_thread_readings=False): + """ + Print out the value of ``self._threads`` or ``self._filtered_hreads`` + """ + threads = self._filtered_threads if filter else self._threads + for tid in sorted(threads): + if show_thread_readings: + readings = [ + self._readings[rid.split("-")[0]][rid] for rid in self._threads[tid] + ] + try: + thread_reading = ( + ": %s" + % self._reading_command.combine_readings(readings).normalize() + ) + except Exception as e: + thread_reading = ": INVALID: %s" % e.__class__.__name__ + else: + thread_reading = "" + + print("%s:" % tid, self._threads[tid], thread_reading) + + def readings( + self, + sentence=None, + threaded=False, + verbose=True, + filter=False, + show_thread_readings=False, + ): + """ + Construct and show the readings of the discourse (or of a single sentence). + + :param sentence: test just this sentence + :type sentence: str + :param threaded: if ``True``, print out each thread ID and the corresponding thread. + :param filter: if ``True``, only print out consistent thread IDs and threads. + """ + self._construct_readings() + self._construct_threads() + + # if we are filtering or showing thread readings, show threads + if filter or show_thread_readings: + threaded = True + + if verbose: + if not threaded: + self._show_readings(sentence=sentence) + else: + self._show_threads( + filter=filter, show_thread_readings=show_thread_readings + ) + + def expand_threads(self, thread_id, threads=None): + """ + Given a thread ID, find the list of ``logic.Expression`` objects corresponding to the reading IDs in that thread. + + :param thread_id: thread ID + :type thread_id: str + :param threads: a mapping from thread IDs to lists of reading IDs + :type threads: dict + :return: A list of pairs ``(rid, reading)`` where reading is the ``logic.Expression`` associated with a reading ID + :rtype: list of tuple + """ + if threads is None: + threads = self._threads + return [ + (rid, self._readings[sid][rid]) + for rid in threads[thread_id] + for sid in rid.split("-")[:1] + ] + + ############################### + # Models and Background + ############################### + + def _check_consistency(self, threads, show=False, verbose=False): + results = [] + for tid in sorted(threads): + assumptions = [ + reading for (rid, reading) in self.expand_threads(tid, threads=threads) + ] + assumptions = list( + map( + self._reading_command.to_fol, + self._reading_command.process_thread(assumptions), + ) + ) + if assumptions: + assumptions += self._background + # if Mace4 finds a model, it always seems to find it quickly + mb = MaceCommand(None, assumptions, max_models=20) + modelfound = mb.build_model() + else: + modelfound = False + results.append((tid, modelfound)) + if show: + spacer(80) + print("Model for Discourse Thread %s" % tid) + spacer(80) + if verbose: + for a in assumptions: + print(a) + spacer(80) + if modelfound: + print(mb.model(format="cooked")) + else: + print("No model found!\n") + return results + + def models(self, thread_id=None, show=True, verbose=False): + """ + Call Mace4 to build a model for each current discourse thread. + + :param thread_id: thread ID + :type thread_id: str + :param show: If ``True``, display the model that has been found. + """ + self._construct_readings() + self._construct_threads() + threads = {thread_id: self._threads[thread_id]} if thread_id else self._threads + + for tid, modelfound in self._check_consistency( + threads, show=show, verbose=verbose + ): + idlist = [rid for rid in threads[tid]] + + if not modelfound: + print(f"Inconsistent discourse: {tid} {idlist}:") + for rid, reading in self.expand_threads(tid): + print(f" {rid}: {reading.normalize()}") + print() + else: + print(f"Consistent discourse: {tid} {idlist}:") + for rid, reading in self.expand_threads(tid): + print(f" {rid}: {reading.normalize()}") + print() + + def add_background(self, background, verbose=False): + """ + Add a list of background assumptions for reasoning about the discourse. + + When called, this method also updates the discourse model's set of readings and threads. + :param background: Formulas which contain background information + :type background: list(Expression) + """ + from nltk.sem.logic import Expression + + for count, e in enumerate(background): + assert isinstance(e, Expression) + if verbose: + print("Adding assumption %s to background" % count) + self._background.append(e) + + # update the state + self._construct_readings() + self._construct_threads() + + def background(self): + """ + Show the current background assumptions. + """ + for e in self._background: + print(str(e)) + + ############################### + # Misc + ############################### + + @staticmethod + def multiply(discourse, readings): + """ + Multiply every thread in ``discourse`` by every reading in ``readings``. + + Given discourse = [['A'], ['B']], readings = ['a', 'b', 'c'] , returns + [['A', 'a'], ['A', 'b'], ['A', 'c'], ['B', 'a'], ['B', 'b'], ['B', 'c']] + + :param discourse: the current list of readings + :type discourse: list of lists + :param readings: an additional list of readings + :type readings: list(Expression) + :rtype: A list of lists + """ + result = [] + for sublist in discourse: + for r in readings: + new = [] + new += sublist + new.append(r) + result.append(new) + return result + + +def load_fol(s): + """ + Temporarily duplicated from ``nltk.sem.util``. + Convert a file of first order formulas into a list of ``Expression`` objects. + + :param s: the contents of the file + :type s: str + :return: a list of parsed formulas. + :rtype: list(Expression) + """ + statements = [] + for linenum, line in enumerate(s.splitlines()): + line = line.strip() + if line.startswith("#") or line == "": + continue + try: + statements.append(Expression.fromstring(line)) + except Exception as e: + raise ValueError(f"Unable to parse line {linenum}: {line}") from e + return statements + + +############################### +# Demo +############################### +def discourse_demo(reading_command=None): + """ + Illustrate the various methods of ``DiscourseTester`` + """ + dt = DiscourseTester( + ["A boxer walks", "Every boxer chases a girl"], reading_command + ) + dt.models() + print() + # dt.grammar() + print() + dt.sentences() + print() + dt.readings() + print() + dt.readings(threaded=True) + print() + dt.models("d1") + dt.add_sentence("John is a boxer") + print() + dt.sentences() + print() + dt.readings(threaded=True) + print() + dt = DiscourseTester( + ["A student dances", "Every student is a person"], reading_command + ) + print() + dt.add_sentence("No person dances", consistchk=True) + print() + dt.readings() + print() + dt.retract_sentence("No person dances", verbose=True) + print() + dt.models() + print() + dt.readings("A person dances") + print() + dt.add_sentence("A person dances", informchk=True) + dt = DiscourseTester( + ["Vincent is a boxer", "Fido is a boxer", "Vincent is married", "Fido barks"], + reading_command, + ) + dt.readings(filter=True) + import nltk.data + + background_file = os.path.join("grammars", "book_grammars", "background.fol") + background = nltk.data.load(background_file) + + print() + dt.add_background(background, verbose=False) + dt.background() + print() + dt.readings(filter=True) + print() + dt.models() + + +def drt_discourse_demo(reading_command=None): + """ + Illustrate the various methods of ``DiscourseTester`` + """ + dt = DiscourseTester(["every dog chases a boy", "he runs"], reading_command) + dt.models() + print() + dt.sentences() + print() + dt.readings() + print() + dt.readings(show_thread_readings=True) + print() + dt.readings(filter=True, show_thread_readings=True) + + +def spacer(num=30): + print("-" * num) + + +def demo(): + discourse_demo() + + tagger = RegexpTagger( + [ + ("^(chases|runs)$", "VB"), + ("^(a)$", "ex_quant"), + ("^(every)$", "univ_quant"), + ("^(dog|boy)$", "NN"), + ("^(he)$", "PRP"), + ] + ) + depparser = MaltParser(tagger=tagger) + drt_discourse_demo( + DrtGlueReadingCommand(remove_duplicates=False, depparser=depparser) + ) + + +if __name__ == "__main__": + demo() diff --git a/nltk/inference/mace.py b/nltk/inference/mace.py new file mode 100644 index 0000000..370abbd --- /dev/null +++ b/nltk/inference/mace.py @@ -0,0 +1,383 @@ +# Natural Language Toolkit: Interface to the Mace4 Model Builder +# +# Author: Dan Garrette +# Ewan Klein + +# URL: +# For license information, see LICENSE.TXT + +""" +A model builder that makes use of the external 'Mace4' package. +""" + +import os +import tempfile + +from nltk.inference.api import BaseModelBuilderCommand, ModelBuilder +from nltk.inference.prover9 import Prover9CommandParent, Prover9Parent +from nltk.sem import Expression, Valuation +from nltk.sem.logic import is_indvar + + +class MaceCommand(Prover9CommandParent, BaseModelBuilderCommand): + """ + A ``MaceCommand`` specific to the ``Mace`` model builder. It contains + a print_assumptions() method that is used to print the list + of assumptions in multiple formats. + """ + + _interpformat_bin = None + + def __init__(self, goal=None, assumptions=None, max_models=500, model_builder=None): + """ + :param goal: Input expression to prove + :type goal: sem.Expression + :param assumptions: Input expressions to use as assumptions in + the proof. + :type assumptions: list(sem.Expression) + :param max_models: The maximum number of models that Mace will try before + simply returning false. (Use 0 for no maximum.) + :type max_models: int + """ + if model_builder is not None: + assert isinstance(model_builder, Mace) + else: + model_builder = Mace(max_models) + + BaseModelBuilderCommand.__init__(self, model_builder, goal, assumptions) + + @property + def valuation(mbc): + return mbc.model("valuation") + + def _convert2val(self, valuation_str): + """ + Transform the output file into an NLTK-style Valuation. + + :return: A model if one is generated; None otherwise. + :rtype: sem.Valuation + """ + valuation_standard_format = self._transform_output(valuation_str, "standard") + + val = [] + for line in valuation_standard_format.splitlines(False): + l = line.strip() + + if l.startswith("interpretation"): + # find the number of entities in the model + num_entities = int(l[l.index("(") + 1 : l.index(",")].strip()) + + elif l.startswith("function") and l.find("_") == -1: + # replace the integer identifier with a corresponding alphabetic character + name = l[l.index("(") + 1 : l.index(",")].strip() + if is_indvar(name): + name = name.upper() + value = int(l[l.index("[") + 1 : l.index("]")].strip()) + val.append((name, MaceCommand._make_model_var(value))) + + elif l.startswith("relation"): + l = l[l.index("(") + 1 :] + if "(" in l: + # relation is not nullary + name = l[: l.index("(")].strip() + values = [ + int(v.strip()) + for v in l[l.index("[") + 1 : l.index("]")].split(",") + ] + val.append( + (name, MaceCommand._make_relation_set(num_entities, values)) + ) + else: + # relation is nullary + name = l[: l.index(",")].strip() + value = int(l[l.index("[") + 1 : l.index("]")].strip()) + val.append((name, value == 1)) + + return Valuation(val) + + @staticmethod + def _make_relation_set(num_entities, values): + """ + Convert a Mace4-style relation table into a dictionary. + + :param num_entities: the number of entities in the model; determines the row length in the table. + :type num_entities: int + :param values: a list of 1's and 0's that represent whether a relation holds in a Mace4 model. + :type values: list of int + """ + r = set() + for position in [pos for (pos, v) in enumerate(values) if v == 1]: + r.add( + tuple(MaceCommand._make_relation_tuple(position, values, num_entities)) + ) + return r + + @staticmethod + def _make_relation_tuple(position, values, num_entities): + if len(values) == 1: + return [] + else: + sublist_size = len(values) // num_entities + sublist_start = position // sublist_size + sublist_position = int(position % sublist_size) + + sublist = values[ + sublist_start * sublist_size : (sublist_start + 1) * sublist_size + ] + return [ + MaceCommand._make_model_var(sublist_start) + ] + MaceCommand._make_relation_tuple( + sublist_position, sublist, num_entities + ) + + @staticmethod + def _make_model_var(value): + """ + Pick an alphabetic character as identifier for an entity in the model. + + :param value: where to index into the list of characters + :type value: int + """ + letter = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + ][value] + num = value // 26 + return letter + str(num) if num > 0 else letter + + def _decorate_model(self, valuation_str, format): + """ + Print out a Mace4 model using any Mace4 ``interpformat`` format. + See https://www.cs.unm.edu/~mccune/mace4/manual/ for details. + + :param valuation_str: str with the model builder's output + :param format: str indicating the format for displaying + models. Defaults to 'standard' format. + :return: str + """ + if not format: + return valuation_str + elif format == "valuation": + return self._convert2val(valuation_str) + else: + return self._transform_output(valuation_str, format) + + def _transform_output(self, valuation_str, format): + """ + Transform the output file into any Mace4 ``interpformat`` format. + + :param format: Output format for displaying models. + :type format: str + """ + if format in [ + "standard", + "standard2", + "portable", + "tabular", + "raw", + "cooked", + "xml", + "tex", + ]: + return self._call_interpformat(valuation_str, [format])[0] + else: + raise LookupError("The specified format does not exist") + + def _call_interpformat(self, input_str, args=[], verbose=False): + """ + Call the ``interpformat`` binary with the given input. + + :param input_str: A string whose contents are used as stdin. + :param args: A list of command-line arguments. + :return: A tuple (stdout, returncode) + :see: ``config_prover9`` + """ + if self._interpformat_bin is None: + self._interpformat_bin = self._modelbuilder._find_binary( + "interpformat", verbose + ) + + return self._modelbuilder._call( + input_str, self._interpformat_bin, args, verbose + ) + + +class Mace(Prover9Parent, ModelBuilder): + _mace4_bin = None + + def __init__(self, end_size=500): + self._end_size = end_size + """The maximum model size that Mace will try before + simply returning false. (Use -1 for no maximum.)""" + + def _build_model(self, goal=None, assumptions=None, verbose=False): + """ + Use Mace4 to build a first order model. + + :return: ``True`` if a model was found (i.e. Mace returns value of 0), + else ``False`` + """ + if not assumptions: + assumptions = [] + + stdout, returncode = self._call_mace4( + self.prover9_input(goal, assumptions), verbose=verbose + ) + return (returncode == 0, stdout) + + def _call_mace4(self, input_str, args=[], verbose=False): + """ + Call the ``mace4`` binary with the given input. + + :param input_str: A string whose contents are used as stdin. + :param args: A list of command-line arguments. + :return: A tuple (stdout, returncode) + :see: ``config_prover9`` + """ + if self._mace4_bin is None: + self._mace4_bin = self._find_binary("mace4", verbose) + + updated_input_str = "" + if self._end_size > 0: + updated_input_str += "assign(end_size, %d).\n\n" % self._end_size + updated_input_str += input_str + + return self._call(updated_input_str, self._mace4_bin, args, verbose) + + +def spacer(num=30): + print("-" * num) + + +def decode_result(found): + """ + Decode the result of model_found() + + :param found: The output of model_found() + :type found: bool + """ + return {True: "Countermodel found", False: "No countermodel found", None: "None"}[ + found + ] + + +def test_model_found(arguments): + """ + Try some proofs and exhibit the results. + """ + for goal, assumptions in arguments: + g = Expression.fromstring(goal) + alist = [lp.parse(a) for a in assumptions] + m = MaceCommand(g, assumptions=alist, max_models=50) + found = m.build_model() + for a in alist: + print(" %s" % a) + print(f"|- {g}: {decode_result(found)}\n") + + +def test_build_model(arguments): + """ + Try to build a ``nltk.sem.Valuation``. + """ + g = Expression.fromstring("all x.man(x)") + alist = [ + Expression.fromstring(a) + for a in [ + "man(John)", + "man(Socrates)", + "man(Bill)", + "some x.(-(x = John) & man(x) & sees(John,x))", + "some x.(-(x = Bill) & man(x))", + "all x.some y.(man(x) -> gives(Socrates,x,y))", + ] + ] + + m = MaceCommand(g, assumptions=alist) + m.build_model() + spacer() + print("Assumptions and Goal") + spacer() + for a in alist: + print(" %s" % a) + print(f"|- {g}: {decode_result(m.build_model())}\n") + spacer() + # print(m.model('standard')) + # print(m.model('cooked')) + print("Valuation") + spacer() + print(m.valuation, "\n") + + +def test_transform_output(argument_pair): + """ + Transform the model into various Mace4 ``interpformat`` formats. + """ + g = Expression.fromstring(argument_pair[0]) + alist = [lp.parse(a) for a in argument_pair[1]] + m = MaceCommand(g, assumptions=alist) + m.build_model() + for a in alist: + print(" %s" % a) + print(f"|- {g}: {m.build_model()}\n") + for format in ["standard", "portable", "xml", "cooked"]: + spacer() + print("Using '%s' format" % format) + spacer() + print(m.model(format=format)) + + +def test_make_relation_set(): + print( + MaceCommand._make_relation_set(num_entities=3, values=[1, 0, 1]) + == {("c",), ("a",)} + ) + print( + MaceCommand._make_relation_set( + num_entities=3, values=[0, 0, 0, 0, 0, 0, 1, 0, 0] + ) + == {("c", "a")} + ) + print( + MaceCommand._make_relation_set(num_entities=2, values=[0, 0, 1, 0, 0, 0, 1, 0]) + == {("a", "b", "a"), ("b", "b", "a")} + ) + + +arguments = [ + ("mortal(Socrates)", ["all x.(man(x) -> mortal(x))", "man(Socrates)"]), + ("(not mortal(Socrates))", ["all x.(man(x) -> mortal(x))", "man(Socrates)"]), +] + + +def demo(): + test_model_found(arguments) + test_build_model(arguments) + test_transform_output(arguments[1]) + + +if __name__ == "__main__": + demo() diff --git a/nltk/inference/nonmonotonic.py b/nltk/inference/nonmonotonic.py new file mode 100644 index 0000000..a69132e --- /dev/null +++ b/nltk/inference/nonmonotonic.py @@ -0,0 +1,561 @@ +# Natural Language Toolkit: Nonmonotonic Reasoning +# +# Author: Daniel H. Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +A module to perform nonmonotonic reasoning. The ideas and demonstrations in +this module are based on "Logical Foundations of Artificial Intelligence" by +Michael R. Genesereth and Nils J. Nilsson. +""" + +from collections import defaultdict +from functools import reduce + +from nltk.inference.api import Prover, ProverCommandDecorator +from nltk.inference.prover9 import Prover9, Prover9Command +from nltk.sem.logic import ( + AbstractVariableExpression, + AllExpression, + AndExpression, + ApplicationExpression, + BooleanExpression, + EqualityExpression, + ExistsExpression, + Expression, + ImpExpression, + NegatedExpression, + Variable, + VariableExpression, + operator, + unique_variable, +) + + +class ProverParseError(Exception): + pass + + +def get_domain(goal, assumptions): + if goal is None: + all_expressions = assumptions + else: + all_expressions = assumptions + [-goal] + return reduce(operator.or_, (a.constants() for a in all_expressions), set()) + + +class ClosedDomainProver(ProverCommandDecorator): + """ + This is a prover decorator that adds domain closure assumptions before + proving. + """ + + def assumptions(self): + assumptions = [a for a in self._command.assumptions()] + goal = self._command.goal() + domain = get_domain(goal, assumptions) + return [self.replace_quants(ex, domain) for ex in assumptions] + + def goal(self): + goal = self._command.goal() + domain = get_domain(goal, self._command.assumptions()) + return self.replace_quants(goal, domain) + + def replace_quants(self, ex, domain): + """ + Apply the closed domain assumption to the expression + + - Domain = union([e.free()|e.constants() for e in all_expressions]) + - translate "exists x.P" to "(z=d1 | z=d2 | ... ) & P.replace(x,z)" OR + "P.replace(x, d1) | P.replace(x, d2) | ..." + - translate "all x.P" to "P.replace(x, d1) & P.replace(x, d2) & ..." + + :param ex: ``Expression`` + :param domain: set of {Variable}s + :return: ``Expression`` + """ + if isinstance(ex, AllExpression): + conjuncts = [ + ex.term.replace(ex.variable, VariableExpression(d)) for d in domain + ] + conjuncts = [self.replace_quants(c, domain) for c in conjuncts] + return reduce(lambda x, y: x & y, conjuncts) + elif isinstance(ex, BooleanExpression): + return ex.__class__( + self.replace_quants(ex.first, domain), + self.replace_quants(ex.second, domain), + ) + elif isinstance(ex, NegatedExpression): + return -self.replace_quants(ex.term, domain) + elif isinstance(ex, ExistsExpression): + disjuncts = [ + ex.term.replace(ex.variable, VariableExpression(d)) for d in domain + ] + disjuncts = [self.replace_quants(d, domain) for d in disjuncts] + return reduce(lambda x, y: x | y, disjuncts) + else: + return ex + + +class UniqueNamesProver(ProverCommandDecorator): + """ + This is a prover decorator that adds unique names assumptions before + proving. + """ + + def assumptions(self): + """ + - Domain = union([e.free()|e.constants() for e in all_expressions]) + - if "d1 = d2" cannot be proven from the premises, then add "d1 != d2" + """ + assumptions = self._command.assumptions() + + domain = list(get_domain(self._command.goal(), assumptions)) + + # build a dictionary of obvious equalities + eq_sets = SetHolder() + for a in assumptions: + if isinstance(a, EqualityExpression): + av = a.first.variable + bv = a.second.variable + # put 'a' and 'b' in the same set + eq_sets[av].add(bv) + + new_assumptions = [] + for i, a in enumerate(domain): + for b in domain[i + 1 :]: + # if a and b are not already in the same equality set + if b not in eq_sets[a]: + newEqEx = EqualityExpression( + VariableExpression(a), VariableExpression(b) + ) + if Prover9().prove(newEqEx, assumptions): + # we can prove that the names are the same entity. + # remember that they are equal so we don't re-check. + eq_sets[a].add(b) + else: + # we can't prove it, so assume unique names + new_assumptions.append(-newEqEx) + + return assumptions + new_assumptions + + +class SetHolder(list): + """ + A list of sets of Variables. + """ + + def __getitem__(self, item): + """ + :param item: ``Variable`` + :return: the set containing 'item' + """ + assert isinstance(item, Variable) + for s in self: + if item in s: + return s + # item is not found in any existing set. so create a new set + new = {item} + self.append(new) + return new + + +class ClosedWorldProver(ProverCommandDecorator): + """ + This is a prover decorator that completes predicates before proving. + + If the assumptions contain "P(A)", then "all x.(P(x) -> (x=A))" is the completion of "P". + If the assumptions contain "all x.(ostrich(x) -> bird(x))", then "all x.(bird(x) -> ostrich(x))" is the completion of "bird". + If the assumptions don't contain anything that are "P", then "all x.-P(x)" is the completion of "P". + + walk(Socrates) + Socrates != Bill + + all x.(walk(x) -> (x=Socrates)) + ---------------- + -walk(Bill) + + see(Socrates, John) + see(John, Mary) + Socrates != John + John != Mary + + all x.all y.(see(x,y) -> ((x=Socrates & y=John) | (x=John & y=Mary))) + ---------------- + -see(Socrates, Mary) + + all x.(ostrich(x) -> bird(x)) + bird(Tweety) + -ostrich(Sam) + Sam != Tweety + + all x.(bird(x) -> (ostrich(x) | x=Tweety)) + + all x.-ostrich(x) + ------------------- + -bird(Sam) + """ + + def assumptions(self): + assumptions = self._command.assumptions() + + predicates = self._make_predicate_dict(assumptions) + + new_assumptions = [] + for p in predicates: + predHolder = predicates[p] + new_sig = self._make_unique_signature(predHolder) + new_sig_exs = [VariableExpression(v) for v in new_sig] + + disjuncts = [] + + # Turn the signatures into disjuncts + for sig in predHolder.signatures: + equality_exs = [] + for v1, v2 in zip(new_sig_exs, sig): + equality_exs.append(EqualityExpression(v1, v2)) + disjuncts.append(reduce(lambda x, y: x & y, equality_exs)) + + # Turn the properties into disjuncts + for prop in predHolder.properties: + # replace variables from the signature with new sig variables + bindings = {} + for v1, v2 in zip(new_sig_exs, prop[0]): + bindings[v2] = v1 + disjuncts.append(prop[1].substitute_bindings(bindings)) + + # make the assumption + if disjuncts: + # disjuncts exist, so make an implication + antecedent = self._make_antecedent(p, new_sig) + consequent = reduce(lambda x, y: x | y, disjuncts) + accum = ImpExpression(antecedent, consequent) + else: + # nothing has property 'p' + accum = NegatedExpression(self._make_antecedent(p, new_sig)) + + # quantify the implication + for new_sig_var in new_sig[::-1]: + accum = AllExpression(new_sig_var, accum) + new_assumptions.append(accum) + + return assumptions + new_assumptions + + def _make_unique_signature(self, predHolder): + """ + This method figures out how many arguments the predicate takes and + returns a tuple containing that number of unique variables. + """ + return tuple(unique_variable() for i in range(predHolder.signature_len)) + + def _make_antecedent(self, predicate, signature): + """ + Return an application expression with 'predicate' as the predicate + and 'signature' as the list of arguments. + """ + antecedent = predicate + for v in signature: + antecedent = antecedent(VariableExpression(v)) + return antecedent + + def _make_predicate_dict(self, assumptions): + """ + Create a dictionary of predicates from the assumptions. + + :param assumptions: a list of ``Expression``s + :return: dict mapping ``AbstractVariableExpression`` to ``PredHolder`` + """ + predicates = defaultdict(PredHolder) + for a in assumptions: + self._map_predicates(a, predicates) + return predicates + + def _map_predicates(self, expression, predDict): + if isinstance(expression, ApplicationExpression): + func, args = expression.uncurry() + if isinstance(func, AbstractVariableExpression): + predDict[func].append_sig(tuple(args)) + elif isinstance(expression, AndExpression): + self._map_predicates(expression.first, predDict) + self._map_predicates(expression.second, predDict) + elif isinstance(expression, AllExpression): + # collect all the universally quantified variables + sig = [expression.variable] + term = expression.term + while isinstance(term, AllExpression): + sig.append(term.variable) + term = term.term + if isinstance(term, ImpExpression): + if isinstance(term.first, ApplicationExpression) and isinstance( + term.second, ApplicationExpression + ): + func1, args1 = term.first.uncurry() + func2, args2 = term.second.uncurry() + if ( + isinstance(func1, AbstractVariableExpression) + and isinstance(func2, AbstractVariableExpression) + and sig == [v.variable for v in args1] + and sig == [v.variable for v in args2] + ): + predDict[func2].append_prop((tuple(sig), term.first)) + predDict[func1].validate_sig_len(sig) + + +class PredHolder: + """ + This class will be used by a dictionary that will store information + about predicates to be used by the ``ClosedWorldProver``. + + The 'signatures' property is a list of tuples defining signatures for + which the predicate is true. For instance, 'see(john, mary)' would be + result in the signature '(john,mary)' for 'see'. + + The second element of the pair is a list of pairs such that the first + element of the pair is a tuple of variables and the second element is an + expression of those variables that makes the predicate true. For instance, + 'all x.all y.(see(x,y) -> know(x,y))' would result in "((x,y),('see(x,y)'))" + for 'know'. + """ + + def __init__(self): + self.signatures = [] + self.properties = [] + self.signature_len = None + + def append_sig(self, new_sig): + self.validate_sig_len(new_sig) + self.signatures.append(new_sig) + + def append_prop(self, new_prop): + self.validate_sig_len(new_prop[0]) + self.properties.append(new_prop) + + def validate_sig_len(self, new_sig): + if self.signature_len is None: + self.signature_len = len(new_sig) + elif self.signature_len != len(new_sig): + raise Exception("Signature lengths do not match") + + def __str__(self): + return f"({self.signatures},{self.properties},{self.signature_len})" + + def __repr__(self): + return "%s" % self + + +def closed_domain_demo(): + lexpr = Expression.fromstring + + p1 = lexpr(r"exists x.walk(x)") + p2 = lexpr(r"man(Socrates)") + c = lexpr(r"walk(Socrates)") + prover = Prover9Command(c, [p1, p2]) + print(prover.prove()) + cdp = ClosedDomainProver(prover) + print("assumptions:") + for a in cdp.assumptions(): + print(" ", a) + print("goal:", cdp.goal()) + print(cdp.prove()) + + p1 = lexpr(r"exists x.walk(x)") + p2 = lexpr(r"man(Socrates)") + p3 = lexpr(r"-walk(Bill)") + c = lexpr(r"walk(Socrates)") + prover = Prover9Command(c, [p1, p2, p3]) + print(prover.prove()) + cdp = ClosedDomainProver(prover) + print("assumptions:") + for a in cdp.assumptions(): + print(" ", a) + print("goal:", cdp.goal()) + print(cdp.prove()) + + p1 = lexpr(r"exists x.walk(x)") + p2 = lexpr(r"man(Socrates)") + p3 = lexpr(r"-walk(Bill)") + c = lexpr(r"walk(Socrates)") + prover = Prover9Command(c, [p1, p2, p3]) + print(prover.prove()) + cdp = ClosedDomainProver(prover) + print("assumptions:") + for a in cdp.assumptions(): + print(" ", a) + print("goal:", cdp.goal()) + print(cdp.prove()) + + p1 = lexpr(r"walk(Socrates)") + p2 = lexpr(r"walk(Bill)") + c = lexpr(r"all x.walk(x)") + prover = Prover9Command(c, [p1, p2]) + print(prover.prove()) + cdp = ClosedDomainProver(prover) + print("assumptions:") + for a in cdp.assumptions(): + print(" ", a) + print("goal:", cdp.goal()) + print(cdp.prove()) + + p1 = lexpr(r"girl(mary)") + p2 = lexpr(r"dog(rover)") + p3 = lexpr(r"all x.(girl(x) -> -dog(x))") + p4 = lexpr(r"all x.(dog(x) -> -girl(x))") + p5 = lexpr(r"chase(mary, rover)") + c = lexpr(r"exists y.(dog(y) & all x.(girl(x) -> chase(x,y)))") + prover = Prover9Command(c, [p1, p2, p3, p4, p5]) + print(prover.prove()) + cdp = ClosedDomainProver(prover) + print("assumptions:") + for a in cdp.assumptions(): + print(" ", a) + print("goal:", cdp.goal()) + print(cdp.prove()) + + +def unique_names_demo(): + lexpr = Expression.fromstring + + p1 = lexpr(r"man(Socrates)") + p2 = lexpr(r"man(Bill)") + c = lexpr(r"exists x.exists y.(x != y)") + prover = Prover9Command(c, [p1, p2]) + print(prover.prove()) + unp = UniqueNamesProver(prover) + print("assumptions:") + for a in unp.assumptions(): + print(" ", a) + print("goal:", unp.goal()) + print(unp.prove()) + + p1 = lexpr(r"all x.(walk(x) -> (x = Socrates))") + p2 = lexpr(r"Bill = William") + p3 = lexpr(r"Bill = Billy") + c = lexpr(r"-walk(William)") + prover = Prover9Command(c, [p1, p2, p3]) + print(prover.prove()) + unp = UniqueNamesProver(prover) + print("assumptions:") + for a in unp.assumptions(): + print(" ", a) + print("goal:", unp.goal()) + print(unp.prove()) + + +def closed_world_demo(): + lexpr = Expression.fromstring + + p1 = lexpr(r"walk(Socrates)") + p2 = lexpr(r"(Socrates != Bill)") + c = lexpr(r"-walk(Bill)") + prover = Prover9Command(c, [p1, p2]) + print(prover.prove()) + cwp = ClosedWorldProver(prover) + print("assumptions:") + for a in cwp.assumptions(): + print(" ", a) + print("goal:", cwp.goal()) + print(cwp.prove()) + + p1 = lexpr(r"see(Socrates, John)") + p2 = lexpr(r"see(John, Mary)") + p3 = lexpr(r"(Socrates != John)") + p4 = lexpr(r"(John != Mary)") + c = lexpr(r"-see(Socrates, Mary)") + prover = Prover9Command(c, [p1, p2, p3, p4]) + print(prover.prove()) + cwp = ClosedWorldProver(prover) + print("assumptions:") + for a in cwp.assumptions(): + print(" ", a) + print("goal:", cwp.goal()) + print(cwp.prove()) + + p1 = lexpr(r"all x.(ostrich(x) -> bird(x))") + p2 = lexpr(r"bird(Tweety)") + p3 = lexpr(r"-ostrich(Sam)") + p4 = lexpr(r"Sam != Tweety") + c = lexpr(r"-bird(Sam)") + prover = Prover9Command(c, [p1, p2, p3, p4]) + print(prover.prove()) + cwp = ClosedWorldProver(prover) + print("assumptions:") + for a in cwp.assumptions(): + print(" ", a) + print("goal:", cwp.goal()) + print(cwp.prove()) + + +def combination_prover_demo(): + lexpr = Expression.fromstring + + p1 = lexpr(r"see(Socrates, John)") + p2 = lexpr(r"see(John, Mary)") + c = lexpr(r"-see(Socrates, Mary)") + prover = Prover9Command(c, [p1, p2]) + print(prover.prove()) + command = ClosedDomainProver(UniqueNamesProver(ClosedWorldProver(prover))) + for a in command.assumptions(): + print(a) + print(command.prove()) + + +def default_reasoning_demo(): + lexpr = Expression.fromstring + + premises = [] + + # define taxonomy + premises.append(lexpr(r"all x.(elephant(x) -> animal(x))")) + premises.append(lexpr(r"all x.(bird(x) -> animal(x))")) + premises.append(lexpr(r"all x.(dove(x) -> bird(x))")) + premises.append(lexpr(r"all x.(ostrich(x) -> bird(x))")) + premises.append(lexpr(r"all x.(flying_ostrich(x) -> ostrich(x))")) + + # default properties + premises.append( + lexpr(r"all x.((animal(x) & -Ab1(x)) -> -fly(x))") + ) # normal animals don't fly + premises.append( + lexpr(r"all x.((bird(x) & -Ab2(x)) -> fly(x))") + ) # normal birds fly + premises.append( + lexpr(r"all x.((ostrich(x) & -Ab3(x)) -> -fly(x))") + ) # normal ostriches don't fly + + # specify abnormal entities + premises.append(lexpr(r"all x.(bird(x) -> Ab1(x))")) # flight + premises.append(lexpr(r"all x.(ostrich(x) -> Ab2(x))")) # non-flying bird + premises.append(lexpr(r"all x.(flying_ostrich(x) -> Ab3(x))")) # flying ostrich + + # define entities + premises.append(lexpr(r"elephant(E)")) + premises.append(lexpr(r"dove(D)")) + premises.append(lexpr(r"ostrich(O)")) + + # print the assumptions + prover = Prover9Command(None, premises) + command = UniqueNamesProver(ClosedWorldProver(prover)) + for a in command.assumptions(): + print(a) + + print_proof("-fly(E)", premises) + print_proof("fly(D)", premises) + print_proof("-fly(O)", premises) + + +def print_proof(goal, premises): + lexpr = Expression.fromstring + prover = Prover9Command(lexpr(goal), premises) + command = UniqueNamesProver(ClosedWorldProver(prover)) + print(goal, prover.prove(), command.prove()) + + +def demo(): + closed_domain_demo() + unique_names_demo() + closed_world_demo() + combination_prover_demo() + default_reasoning_demo() + + +if __name__ == "__main__": + demo() diff --git a/nltk/inference/prover9.py b/nltk/inference/prover9.py new file mode 100644 index 0000000..b4729d6 --- /dev/null +++ b/nltk/inference/prover9.py @@ -0,0 +1,507 @@ +# Natural Language Toolkit: Interface to the Prover9 Theorem Prover +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Dan Garrette +# Ewan Klein +# +# URL: +# For license information, see LICENSE.TXT +""" +A theorem prover that makes use of the external 'Prover9' package. +""" + +import os +import subprocess + +import nltk +from nltk.inference.api import BaseProverCommand, Prover +from nltk.sem.logic import ( + AllExpression, + AndExpression, + EqualityExpression, + ExistsExpression, + Expression, + IffExpression, + ImpExpression, + NegatedExpression, + OrExpression, +) + +# +# Following is not yet used. Return code for 2 actually realized as 512. +# +p9_return_codes = { + 0: True, + 1: "(FATAL)", # A fatal error occurred (user's syntax error). + 2: False, # (SOS_EMPTY) Prover9 ran out of things to do + # (sos list exhausted). + 3: "(MAX_MEGS)", # The max_megs (memory limit) parameter was exceeded. + 4: "(MAX_SECONDS)", # The max_seconds parameter was exceeded. + 5: "(MAX_GIVEN)", # The max_given parameter was exceeded. + 6: "(MAX_KEPT)", # The max_kept parameter was exceeded. + 7: "(ACTION)", # A Prover9 action terminated the search. + 101: "(SIGSEGV)", # Prover9 crashed, most probably due to a bug. +} + + +class Prover9CommandParent: + """ + A common base class used by both ``Prover9Command`` and ``MaceCommand``, + which is responsible for maintaining a goal and a set of assumptions, + and generating prover9-style input files from them. + """ + + def print_assumptions(self, output_format="nltk"): + """ + Print the list of the current assumptions. + """ + if output_format.lower() == "nltk": + for a in self.assumptions(): + print(a) + elif output_format.lower() == "prover9": + for a in convert_to_prover9(self.assumptions()): + print(a) + else: + raise NameError( + "Unrecognized value for 'output_format': %s" % output_format + ) + + +class Prover9Command(Prover9CommandParent, BaseProverCommand): + """ + A ``ProverCommand`` specific to the ``Prover9`` prover. It contains + the a print_assumptions() method that is used to print the list + of assumptions in multiple formats. + """ + + def __init__(self, goal=None, assumptions=None, timeout=60, prover=None): + """ + :param goal: Input expression to prove + :type goal: sem.Expression + :param assumptions: Input expressions to use as assumptions in + the proof. + :type assumptions: list(sem.Expression) + :param timeout: number of seconds before timeout; set to 0 for + no timeout. + :type timeout: int + :param prover: a prover. If not set, one will be created. + :type prover: Prover9 + """ + if not assumptions: + assumptions = [] + + if prover is not None: + assert isinstance(prover, Prover9) + else: + prover = Prover9(timeout) + + BaseProverCommand.__init__(self, prover, goal, assumptions) + + def decorate_proof(self, proof_string, simplify=True): + """ + :see BaseProverCommand.decorate_proof() + """ + if simplify: + return self._prover._call_prooftrans(proof_string, ["striplabels"])[ + 0 + ].rstrip() + else: + return proof_string.rstrip() + + +class Prover9Parent: + """ + A common class extended by both ``Prover9`` and ``Mace ``. + It contains the functionality required to convert NLTK-style + expressions into Prover9-style expressions. + """ + + _binary_location = None + + def config_prover9(self, binary_location, verbose=False): + if binary_location is None: + self._binary_location = None + self._prover9_bin = None + else: + name = "prover9" + self._prover9_bin = nltk.internals.find_binary( + name, + path_to_bin=binary_location, + env_vars=["PROVER9"], + url="https://www.cs.unm.edu/~mccune/prover9/", + binary_names=[name, name + ".exe"], + verbose=verbose, + ) + self._binary_location = self._prover9_bin.rsplit(os.path.sep, 1) + + def prover9_input(self, goal, assumptions): + """ + :return: The input string that should be provided to the + prover9 binary. This string is formed based on the goal, + assumptions, and timeout value of this object. + """ + s = "" + + if assumptions: + s += "formulas(assumptions).\n" + for p9_assumption in convert_to_prover9(assumptions): + s += " %s.\n" % p9_assumption + s += "end_of_list.\n\n" + + if goal: + s += "formulas(goals).\n" + s += " %s.\n" % convert_to_prover9(goal) + s += "end_of_list.\n\n" + + return s + + def binary_locations(self): + """ + A list of directories that should be searched for the prover9 + executables. This list is used by ``config_prover9`` when searching + for the prover9 executables. + """ + return [ + "/usr/local/bin/prover9", + "/usr/local/bin/prover9/bin", + "/usr/local/bin", + "/usr/bin", + "/usr/local/prover9", + "/usr/local/share/prover9", + ] + + def _find_binary(self, name, verbose=False): + binary_locations = self.binary_locations() + if self._binary_location is not None: + binary_locations += [self._binary_location] + return nltk.internals.find_binary( + name, + searchpath=binary_locations, + env_vars=["PROVER9"], + url="https://www.cs.unm.edu/~mccune/prover9/", + binary_names=[name, name + ".exe"], + verbose=verbose, + ) + + def _call(self, input_str, binary, args=[], verbose=False): + """ + Call the binary with the given input. + + :param input_str: A string whose contents are used as stdin. + :param binary: The location of the binary to call + :param args: A list of command-line arguments. + :return: A tuple (stdout, returncode) + :see: ``config_prover9`` + """ + if verbose: + print("Calling:", binary) + print("Args:", args) + print("Input:\n", input_str, "\n") + + # Call prover9 via a subprocess + cmd = [binary] + args + try: + input_str = input_str.encode("utf8") + except AttributeError: + pass + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE + ) + (stdout, stderr) = p.communicate(input=input_str) + + if verbose: + print("Return code:", p.returncode) + if stdout: + print("stdout:\n", stdout, "\n") + if stderr: + print("stderr:\n", stderr, "\n") + + return (stdout.decode("utf-8"), p.returncode) + + +def convert_to_prover9(input): + """ + Convert a ``logic.Expression`` to Prover9 format. + """ + if isinstance(input, list): + result = [] + for s in input: + try: + result.append(_convert_to_prover9(s.simplify())) + except Exception: + print("input %s cannot be converted to Prover9 input syntax" % input) + raise + return result + else: + try: + return _convert_to_prover9(input.simplify()) + except Exception: + print("input %s cannot be converted to Prover9 input syntax" % input) + raise + + +def _convert_to_prover9(expression): + """ + Convert ``logic.Expression`` to Prover9 formatted string. + """ + if isinstance(expression, ExistsExpression): + return ( + "exists " + + str(expression.variable) + + " " + + _convert_to_prover9(expression.term) + ) + elif isinstance(expression, AllExpression): + return ( + "all " + + str(expression.variable) + + " " + + _convert_to_prover9(expression.term) + ) + elif isinstance(expression, NegatedExpression): + return "-(" + _convert_to_prover9(expression.term) + ")" + elif isinstance(expression, AndExpression): + return ( + "(" + + _convert_to_prover9(expression.first) + + " & " + + _convert_to_prover9(expression.second) + + ")" + ) + elif isinstance(expression, OrExpression): + return ( + "(" + + _convert_to_prover9(expression.first) + + " | " + + _convert_to_prover9(expression.second) + + ")" + ) + elif isinstance(expression, ImpExpression): + return ( + "(" + + _convert_to_prover9(expression.first) + + " -> " + + _convert_to_prover9(expression.second) + + ")" + ) + elif isinstance(expression, IffExpression): + return ( + "(" + + _convert_to_prover9(expression.first) + + " <-> " + + _convert_to_prover9(expression.second) + + ")" + ) + elif isinstance(expression, EqualityExpression): + return ( + "(" + + _convert_to_prover9(expression.first) + + " = " + + _convert_to_prover9(expression.second) + + ")" + ) + else: + return str(expression) + + +class Prover9(Prover9Parent, Prover): + _prover9_bin = None + _prooftrans_bin = None + + def __init__(self, timeout=60): + self._timeout = timeout + """The timeout value for prover9. If a proof can not be found + in this amount of time, then prover9 will return false. + (Use 0 for no timeout.)""" + + def _prove(self, goal=None, assumptions=None, verbose=False): + """ + Use Prover9 to prove a theorem. + :return: A pair whose first element is a boolean indicating if the + proof was successful (i.e. returns value of 0) and whose second element + is the output of the prover. + """ + if not assumptions: + assumptions = [] + + stdout, returncode = self._call_prover9( + self.prover9_input(goal, assumptions), verbose=verbose + ) + return (returncode == 0, stdout) + + def prover9_input(self, goal, assumptions): + """ + :see: Prover9Parent.prover9_input + """ + s = "clear(auto_denials).\n" # only one proof required + return s + Prover9Parent.prover9_input(self, goal, assumptions) + + def _call_prover9(self, input_str, args=[], verbose=False): + """ + Call the ``prover9`` binary with the given input. + + :param input_str: A string whose contents are used as stdin. + :param args: A list of command-line arguments. + :return: A tuple (stdout, returncode) + :see: ``config_prover9`` + """ + if self._prover9_bin is None: + self._prover9_bin = self._find_binary("prover9", verbose) + + updated_input_str = "" + if self._timeout > 0: + updated_input_str += "assign(max_seconds, %d).\n\n" % self._timeout + updated_input_str += input_str + + stdout, returncode = self._call( + updated_input_str, self._prover9_bin, args, verbose + ) + + if returncode not in [0, 2]: + errormsgprefix = "%%ERROR:" + if errormsgprefix in stdout: + msgstart = stdout.index(errormsgprefix) + errormsg = stdout[msgstart:].strip() + else: + errormsg = None + if returncode in [3, 4, 5, 6]: + raise Prover9LimitExceededException(returncode, errormsg) + else: + raise Prover9FatalException(returncode, errormsg) + + return stdout, returncode + + def _call_prooftrans(self, input_str, args=[], verbose=False): + """ + Call the ``prooftrans`` binary with the given input. + + :param input_str: A string whose contents are used as stdin. + :param args: A list of command-line arguments. + :return: A tuple (stdout, returncode) + :see: ``config_prover9`` + """ + if self._prooftrans_bin is None: + self._prooftrans_bin = self._find_binary("prooftrans", verbose) + + return self._call(input_str, self._prooftrans_bin, args, verbose) + + +class Prover9Exception(Exception): + def __init__(self, returncode, message): + msg = p9_return_codes[returncode] + if message: + msg += "\n%s" % message + Exception.__init__(self, msg) + + +class Prover9FatalException(Prover9Exception): + pass + + +class Prover9LimitExceededException(Prover9Exception): + pass + + +###################################################################### +# { Tests and Demos +###################################################################### + + +def test_config(): + a = Expression.fromstring("(walk(j) & sing(j))") + g = Expression.fromstring("walk(j)") + p = Prover9Command(g, assumptions=[a]) + p._executable_path = None + p.prover9_search = [] + p.prove() + # config_prover9('/usr/local/bin') + print(p.prove()) + print(p.proof()) + + +def test_convert_to_prover9(expr): + """ + Test that parsing works OK. + """ + for t in expr: + e = Expression.fromstring(t) + print(convert_to_prover9(e)) + + +def test_prove(arguments): + """ + Try some proofs and exhibit the results. + """ + for goal, assumptions in arguments: + g = Expression.fromstring(goal) + alist = [Expression.fromstring(a) for a in assumptions] + p = Prover9Command(g, assumptions=alist).prove() + for a in alist: + print(" %s" % a) + print(f"|- {g}: {p}\n") + + +arguments = [ + ("(man(x) <-> (not (not man(x))))", []), + ("(not (man(x) & (not man(x))))", []), + ("(man(x) | (not man(x)))", []), + ("(man(x) & (not man(x)))", []), + ("(man(x) -> man(x))", []), + ("(not (man(x) & (not man(x))))", []), + ("(man(x) | (not man(x)))", []), + ("(man(x) -> man(x))", []), + ("(man(x) <-> man(x))", []), + ("(not (man(x) <-> (not man(x))))", []), + ("mortal(Socrates)", ["all x.(man(x) -> mortal(x))", "man(Socrates)"]), + ("((all x.(man(x) -> walks(x)) & man(Socrates)) -> some y.walks(y))", []), + ("(all x.man(x) -> all x.man(x))", []), + ("some x.all y.sees(x,y)", []), + ( + "some e3.(walk(e3) & subj(e3, mary))", + [ + "some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))" + ], + ), + ( + "some x e1.(see(e1) & subj(e1, x) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))", + [ + "some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))" + ], + ), +] + +expressions = [ + r"some x y.sees(x,y)", + r"some x.(man(x) & walks(x))", + r"\x.(man(x) & walks(x))", + r"\x y.sees(x,y)", + r"walks(john)", + r"\x.big(x, \y.mouse(y))", + r"(walks(x) & (runs(x) & (threes(x) & fours(x))))", + r"(walks(x) -> runs(x))", + r"some x.(PRO(x) & sees(John, x))", + r"some x.(man(x) & (not walks(x)))", + r"all x.(man(x) -> walks(x))", +] + + +def spacer(num=45): + print("-" * num) + + +def demo(): + print("Testing configuration") + spacer() + test_config() + print() + print("Testing conversion to Prover9 format") + spacer() + test_convert_to_prover9(expressions) + print() + print("Testing proofs") + spacer() + test_prove(arguments) + + +if __name__ == "__main__": + demo() diff --git a/nltk/inference/resolution.py b/nltk/inference/resolution.py new file mode 100755 index 0000000..ec4dc78 --- /dev/null +++ b/nltk/inference/resolution.py @@ -0,0 +1,788 @@ +# Natural Language Toolkit: First-order Resolution-based Theorem Prover +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +Module for a resolution-based First Order theorem prover. +""" + +import operator +import time +from collections import defaultdict +from functools import reduce + +from nltk.inference.api import BaseProverCommand, Prover +from nltk.sem import skolemize +from nltk.sem.logic import ( + AndExpression, + ApplicationExpression, + EqualityExpression, + Expression, + IndividualVariableExpression, + NegatedExpression, + OrExpression, + Variable, + VariableExpression, + is_indvar, + unique_variable, +) + + +class ProverParseError(Exception): + pass + + +class ResolutionProver(Prover): + ANSWER_KEY = "ANSWER" + _assume_false = True + #: Wall-clock limit, in seconds, on a single proof search. First-order + #: resolution is only semi-decidable, so a satisfiable goal makes the + #: saturation loop derive and append resolvents indefinitely and pin a CPU + #: core (CWE-400). Mirroring :class:`nltk.inference.Prover9`'s ``timeout``, + #: the search is abandoned and the goal reported unproved once this many + #: seconds elapse; set it to ``0`` to disable the limit (the original, + #: unbounded behaviour). + TIMEOUT = 60 + + def _prove(self, goal=None, assumptions=None, verbose=False): + """ + :param goal: Input expression to prove + :type goal: sem.Expression + :param assumptions: Input expressions to use as assumptions in the proof + :type assumptions: list(sem.Expression) + """ + if not assumptions: + assumptions = [] + + result = None + try: + clauses = [] + if goal: + clauses.extend(clausify(-goal)) + for a in assumptions: + clauses.extend(clausify(a)) + result, clauses = self._attempt_proof(clauses) + if verbose: + print(ResolutionProverCommand._decorate_clauses(clauses)) + except RuntimeError as e: + if self._assume_false and str(e).startswith( + "maximum recursion depth exceeded" + ): + result = False + clauses = [] + else: + if verbose: + print(e) + else: + raise e + return (result, clauses) + + def _attempt_proof(self, clauses): + # map indices to lists of indices, to store attempted unifications + tried = defaultdict(list) + + # Bound the saturation search by wall-clock time so an unprovable or + # non-terminating goal can't keep deriving resolvents forever and pin a + # CPU core (CWE-400). ``deadline`` is ``None`` when the limit is disabled + # (TIMEOUT == 0). The cost of an individual unification grows as clauses + # accumulate literals, so the deadline is checked on every attempt. + deadline = time.monotonic() + self.TIMEOUT if self.TIMEOUT else None + + i = 0 + while i < len(clauses): + if not clauses[i].is_tautology(): + # since we try clauses in order, we should start after the last + # index tried + if tried[i]: + j = tried[i][-1] + 1 + else: + j = i + 1 # nothing tried yet for 'i', so start with the next + + while j < len(clauses): + # don't: 1) unify a clause with itself, + # 2) use tautologies + if i != j and j and not clauses[j].is_tautology(): + if deadline is not None and time.monotonic() > deadline: + # Time budget exhausted: report unproved with an empty + # clause set, matching the recursion-exhaustion path in + # _prove. Returning the (possibly huge) accumulated + # clauses would make ResolutionProverCommand.prove() + # decorate/stringify all of them, spending time and + # memory beyond TIMEOUT and undermining the bound. + return (False, []) + tried[i].append(j) + newclauses = clauses[i].unify(clauses[j]) + if newclauses: + for newclause in newclauses: + newclause._parents = (i + 1, j + 1) + clauses.append(newclause) + if not len(newclause): # if there's an empty clause + return (True, clauses) + i = -1 # since we added a new clause, restart from the top + break + j += 1 + i += 1 + return (False, clauses) + + +class ResolutionProverCommand(BaseProverCommand): + def __init__(self, goal=None, assumptions=None, prover=None): + """ + :param goal: Input expression to prove + :type goal: sem.Expression + :param assumptions: Input expressions to use as assumptions in + the proof. + :type assumptions: list(sem.Expression) + """ + if prover is not None: + assert isinstance(prover, ResolutionProver) + else: + prover = ResolutionProver() + + BaseProverCommand.__init__(self, prover, goal, assumptions) + self._clauses = None + + def prove(self, verbose=False): + """ + Perform the actual proof. Store the result to prevent unnecessary + re-proving. + """ + if self._result is None: + self._result, clauses = self._prover._prove( + self.goal(), self.assumptions(), verbose + ) + self._clauses = clauses + self._proof = ResolutionProverCommand._decorate_clauses(clauses) + return self._result + + def find_answers(self, verbose=False): + self.prove(verbose) + + answers = set() + answer_ex = VariableExpression(Variable(ResolutionProver.ANSWER_KEY)) + for clause in self._clauses: + if ( + len(clause) == 1 + and isinstance(clause[0], ApplicationExpression) + and clause[0].function == answer_ex + and not isinstance(clause[0].argument, IndividualVariableExpression) + ): + answers.add(clause[0].argument) + return answers + + @staticmethod + def _decorate_clauses(clauses): + """ + Decorate the proof output. + """ + out = "" + # No clauses means no proof to show (e.g. when the search is abandoned on + # timeout or recursion exhaustion). Return early; ``max(...)`` below would + # otherwise raise on an empty sequence. + if not clauses: + return out + max_clause_len = max(len(str(clause)) for clause in clauses) + max_seq_len = len(str(len(clauses))) + for i in range(len(clauses)): + parents = "A" + taut = "" + if clauses[i].is_tautology(): + taut = "Tautology" + if clauses[i]._parents: + parents = str(clauses[i]._parents) + parents = " " * (max_clause_len - len(str(clauses[i])) + 1) + parents + seq = " " * (max_seq_len - len(str(i + 1))) + str(i + 1) + out += f"[{seq}] {clauses[i]} {parents} {taut}\n" + return out + + +class Clause(list): + def __init__(self, data): + list.__init__(self, data) + self._is_tautology = None + self._parents = None + + def unify(self, other, bindings=None, used=None, skipped=None, debug=False): + """ + Attempt to unify this Clause with the other, returning a list of + resulting, unified, Clauses. + + :param other: ``Clause`` with which to unify + :param bindings: ``BindingDict`` containing bindings that should be used + during the unification + :param used: tuple of two lists of atoms. The first lists the + atoms from 'self' that were successfully unified with atoms from + 'other'. The second lists the atoms from 'other' that were successfully + unified with atoms from 'self'. + :param skipped: tuple of two ``Clause`` objects. The first is a list of all + the atoms from the 'self' Clause that have not been unified with + anything on the path. The second is same thing for the 'other' Clause. + :param debug: bool indicating whether debug statements should print + :return: list containing all the resulting ``Clause`` objects that could be + obtained by unification + """ + if bindings is None: + bindings = BindingDict() + if used is None: + used = ([], []) + if skipped is None: + skipped = ([], []) + if isinstance(debug, bool): + debug = DebugObject(debug) + + newclauses = _iterate_first( + self, other, bindings, used, skipped, _complete_unify_path, debug + ) + + # remove subsumed clauses. make a list of all indices of subsumed + # clauses, and then remove them from the list + subsumed = [] + for i, c1 in enumerate(newclauses): + if i not in subsumed: + for j, c2 in enumerate(newclauses): + if i != j and j not in subsumed and c1.subsumes(c2): + subsumed.append(j) + result = [] + for i in range(len(newclauses)): + if i not in subsumed: + result.append(newclauses[i]) + + return result + + def isSubsetOf(self, other): + """ + Return True iff every term in 'self' is a term in 'other'. + + :param other: ``Clause`` + :return: bool + """ + for a in self: + if a not in other: + return False + return True + + def subsumes(self, other): + """ + Return True iff 'self' subsumes 'other', this is, if there is a + substitution such that every term in 'self' can be unified with a term + in 'other'. + + :param other: ``Clause`` + :return: bool + """ + negatedother = [] + for atom in other: + if isinstance(atom, NegatedExpression): + negatedother.append(atom.term) + else: + negatedother.append(-atom) + + negatedotherClause = Clause(negatedother) + + bindings = BindingDict() + used = ([], []) + skipped = ([], []) + debug = DebugObject(False) + + return ( + len( + _iterate_first( + self, + negatedotherClause, + bindings, + used, + skipped, + _subsumes_finalize, + debug, + ) + ) + > 0 + ) + + def __getslice__(self, start, end): + return Clause(list.__getslice__(self, start, end)) + + def __sub__(self, other): + return Clause([a for a in self if a not in other]) + + def __add__(self, other): + return Clause(list.__add__(self, other)) + + def is_tautology(self): + """ + Self is a tautology if it contains ground terms P and -P. The ground + term, P, must be an exact match, ie, not using unification. + """ + if self._is_tautology is not None: + return self._is_tautology + for i, a in enumerate(self): + if not isinstance(a, EqualityExpression): + j = len(self) - 1 + while j > i: + b = self[j] + if isinstance(a, NegatedExpression): + if a.term == b: + self._is_tautology = True + return True + elif isinstance(b, NegatedExpression): + if a == b.term: + self._is_tautology = True + return True + j -= 1 + self._is_tautology = False + return False + + def free(self): + return reduce(operator.or_, ((atom.free() | atom.constants()) for atom in self)) + + def replace(self, variable, expression): + """ + Replace every instance of variable with expression across every atom + in the clause + + :param variable: ``Variable`` + :param expression: ``Expression`` + """ + return Clause([atom.replace(variable, expression) for atom in self]) + + def substitute_bindings(self, bindings): + """ + Replace every binding + + :param bindings: A list of tuples mapping Variable Expressions to the + Expressions to which they are bound. + :return: ``Clause`` + """ + return Clause([atom.substitute_bindings(bindings) for atom in self]) + + def __str__(self): + return "{" + ", ".join("%s" % item for item in self) + "}" + + def __repr__(self): + return "%s" % self + + +def _iterate_first(first, second, bindings, used, skipped, finalize_method, debug): + """ + This method facilitates movement through the terms of 'self' + """ + debug.line(f"unify({first},{second}) {bindings}") + + if not len(first) or not len(second): # if no more recursions can be performed + return finalize_method(first, second, bindings, used, skipped, debug) + else: + # explore this 'self' atom + result = _iterate_second( + first, second, bindings, used, skipped, finalize_method, debug + 1 + ) + + # skip this possible 'self' atom + newskipped = (skipped[0] + [first[0]], skipped[1]) + result += _iterate_first( + first[1:], second, bindings, used, newskipped, finalize_method, debug + 1 + ) + + try: + newbindings, newused, unused = _unify_terms( + first[0], second[0], bindings, used + ) + # Unification found, so progress with this line of unification + # put skipped and unused terms back into play for later unification. + newfirst = first[1:] + skipped[0] + unused[0] + newsecond = second[1:] + skipped[1] + unused[1] + result += _iterate_first( + newfirst, + newsecond, + newbindings, + newused, + ([], []), + finalize_method, + debug + 1, + ) + except BindingException: + # the atoms could not be unified, + pass + + return result + + +def _iterate_second(first, second, bindings, used, skipped, finalize_method, debug): + """ + This method facilitates movement through the terms of 'other' + """ + debug.line(f"unify({first},{second}) {bindings}") + + if not len(first) or not len(second): # if no more recursions can be performed + return finalize_method(first, second, bindings, used, skipped, debug) + else: + # skip this possible pairing and move to the next + newskipped = (skipped[0], skipped[1] + [second[0]]) + result = _iterate_second( + first, second[1:], bindings, used, newskipped, finalize_method, debug + 1 + ) + + try: + newbindings, newused, unused = _unify_terms( + first[0], second[0], bindings, used + ) + # Unification found, so progress with this line of unification + # put skipped and unused terms back into play for later unification. + newfirst = first[1:] + skipped[0] + unused[0] + newsecond = second[1:] + skipped[1] + unused[1] + result += _iterate_second( + newfirst, + newsecond, + newbindings, + newused, + ([], []), + finalize_method, + debug + 1, + ) + except BindingException: + # the atoms could not be unified, + pass + + return result + + +def _unify_terms(a, b, bindings=None, used=None): + """ + This method attempts to unify two terms. Two expressions are unifiable + if there exists a substitution function S such that S(a) == S(-b). + + :param a: ``Expression`` + :param b: ``Expression`` + :param bindings: ``BindingDict`` a starting set of bindings with which + the unification must be consistent + :return: ``BindingDict`` A dictionary of the bindings required to unify + :raise ``BindingException``: If the terms cannot be unified + """ + assert isinstance(a, Expression) + assert isinstance(b, Expression) + + if bindings is None: + bindings = BindingDict() + if used is None: + used = ([], []) + + # Use resolution + if isinstance(a, NegatedExpression) and isinstance(b, ApplicationExpression): + newbindings = most_general_unification(a.term, b, bindings) + newused = (used[0] + [a], used[1] + [b]) + unused = ([], []) + elif isinstance(a, ApplicationExpression) and isinstance(b, NegatedExpression): + newbindings = most_general_unification(a, b.term, bindings) + newused = (used[0] + [a], used[1] + [b]) + unused = ([], []) + + # Use demodulation + elif isinstance(a, EqualityExpression): + newbindings = BindingDict([(a.first.variable, a.second)]) + newused = (used[0] + [a], used[1]) + unused = ([], [b]) + elif isinstance(b, EqualityExpression): + newbindings = BindingDict([(b.first.variable, b.second)]) + newused = (used[0], used[1] + [b]) + unused = ([a], []) + + else: + raise BindingException((a, b)) + + return newbindings, newused, unused + + +def _complete_unify_path(first, second, bindings, used, skipped, debug): + if used[0] or used[1]: # if bindings were made along the path + newclause = Clause(skipped[0] + skipped[1] + first + second) + debug.line(" -> New Clause: %s" % newclause) + return [newclause.substitute_bindings(bindings)] + else: # no bindings made means no unification occurred. so no result + debug.line(" -> End") + return [] + + +def _subsumes_finalize(first, second, bindings, used, skipped, debug): + if not len(skipped[0]) and not len(first): + # If there are no skipped terms and no terms left in 'first', then + # all of the terms in the original 'self' were unified with terms + # in 'other'. Therefore, there exists a binding (this one) such that + # every term in self can be unified with a term in other, which + # is the definition of subsumption. + return [True] + else: + return [] + + +def clausify(expression): + """ + Skolemize, clausify, and standardize the variables apart. + """ + clause_list = [] + for clause in _clausify(skolemize(expression)): + for free in clause.free(): + if is_indvar(free.name): + newvar = VariableExpression(unique_variable()) + clause = clause.replace(free, newvar) + clause_list.append(clause) + return clause_list + + +def _clausify(expression): + """ + :param expression: a skolemized expression in CNF + """ + if isinstance(expression, AndExpression): + return _clausify(expression.first) + _clausify(expression.second) + elif isinstance(expression, OrExpression): + first = _clausify(expression.first) + second = _clausify(expression.second) + assert len(first) == 1 + assert len(second) == 1 + return [first[0] + second[0]] + elif isinstance(expression, EqualityExpression): + return [Clause([expression])] + elif isinstance(expression, ApplicationExpression): + return [Clause([expression])] + elif isinstance(expression, NegatedExpression): + if isinstance(expression.term, ApplicationExpression): + return [Clause([expression])] + elif isinstance(expression.term, EqualityExpression): + return [Clause([expression])] + raise ProverParseError() + + +class BindingDict: + def __init__(self, binding_list=None): + """ + :param binding_list: list of (``AbstractVariableExpression``, ``AtomicExpression``) to initialize the dictionary + """ + self.d = {} + + if binding_list: + for v, b in binding_list: + self[v] = b + + def __setitem__(self, variable, binding): + """ + A binding is consistent with the dict if its variable is not already bound, OR if its + variable is already bound to its argument. + + :param variable: ``Variable`` The variable to bind + :param binding: ``Expression`` The atomic to which 'variable' should be bound + :raise BindingException: If the variable cannot be bound in this dictionary + """ + assert isinstance(variable, Variable) + assert isinstance(binding, Expression) + + try: + existing = self[variable] + except KeyError: + existing = None + + if not existing or binding == existing: + self.d[variable] = binding + elif isinstance(binding, IndividualVariableExpression): + # Since variable is already bound, try to bind binding to variable + try: + existing = self[binding.variable] + except KeyError: + existing = None + + binding2 = VariableExpression(variable) + + if not existing or binding2 == existing: + self.d[binding.variable] = binding2 + else: + raise BindingException( + "Variable %s already bound to another " "value" % (variable) + ) + else: + raise BindingException( + "Variable %s already bound to another " "value" % (variable) + ) + + def __getitem__(self, variable): + """ + Return the expression to which 'variable' is bound + """ + assert isinstance(variable, Variable) + + intermediate = self.d[variable] + while intermediate: + try: + intermediate = self.d[intermediate] + except KeyError: + return intermediate + + def __contains__(self, item): + return item in self.d + + def __add__(self, other): + """ + :param other: ``BindingDict`` The dict with which to combine self + :return: ``BindingDict`` A new dict containing all the elements of both parameters + :raise BindingException: If the parameter dictionaries are not consistent with each other + """ + try: + combined = BindingDict() + for v in self.d: + combined[v] = self.d[v] + for v in other.d: + combined[v] = other.d[v] + return combined + except BindingException as e: + raise BindingException( + "Attempting to add two contradicting " + "BindingDicts: '%s' and '%s'" % (self, other) + ) from e + + def __len__(self): + return len(self.d) + + def __str__(self): + data_str = ", ".join(f"{v}: {self.d[v]}" for v in sorted(self.d.keys())) + return "{" + data_str + "}" + + def __repr__(self): + return "%s" % self + + +def most_general_unification(a, b, bindings=None): + """ + Find the most general unification of the two given expressions + + :param a: ``Expression`` + :param b: ``Expression`` + :param bindings: ``BindingDict`` a starting set of bindings with which the + unification must be consistent + :return: a list of bindings + :raise BindingException: if the Expressions cannot be unified + """ + if bindings is None: + bindings = BindingDict() + + if a == b: + return bindings + elif isinstance(a, IndividualVariableExpression): + return _mgu_var(a, b, bindings) + elif isinstance(b, IndividualVariableExpression): + return _mgu_var(b, a, bindings) + elif isinstance(a, ApplicationExpression) and isinstance(b, ApplicationExpression): + return most_general_unification( + a.function, b.function, bindings + ) + most_general_unification(a.argument, b.argument, bindings) + raise BindingException((a, b)) + + +def _mgu_var(var, expression, bindings): + if var.variable in expression.free() | expression.constants(): + raise BindingException((var, expression)) + else: + return BindingDict([(var.variable, expression)]) + bindings + + +class BindingException(Exception): + def __init__(self, arg): + if isinstance(arg, tuple): + Exception.__init__(self, "'%s' cannot be bound to '%s'" % arg) + else: + Exception.__init__(self, arg) + + +class UnificationException(Exception): + def __init__(self, a, b): + Exception.__init__(self, f"'{a}' cannot unify with '{b}'") + + +class DebugObject: + def __init__(self, enabled=True, indent=0): + self.enabled = enabled + self.indent = indent + + def __add__(self, i): + return DebugObject(self.enabled, self.indent + i) + + def line(self, line): + if self.enabled: + print(" " * self.indent + line) + + +def testResolutionProver(): + resolution_test(r"man(x)") + resolution_test(r"(man(x) -> man(x))") + resolution_test(r"(man(x) -> --man(x))") + resolution_test(r"-(man(x) and -man(x))") + resolution_test(r"(man(x) or -man(x))") + resolution_test(r"(man(x) -> man(x))") + resolution_test(r"-(man(x) and -man(x))") + resolution_test(r"(man(x) or -man(x))") + resolution_test(r"(man(x) -> man(x))") + resolution_test(r"(man(x) iff man(x))") + resolution_test(r"-(man(x) iff -man(x))") + resolution_test("all x.man(x)") + resolution_test("-all x.some y.F(x,y) & some x.all y.(-F(x,y))") + resolution_test("some x.all y.sees(x,y)") + + p1 = Expression.fromstring(r"all x.(man(x) -> mortal(x))") + p2 = Expression.fromstring(r"man(Socrates)") + c = Expression.fromstring(r"mortal(Socrates)") + print(f"{p1}, {p2} |- {c}: {ResolutionProver().prove(c, [p1, p2])}") + + p1 = Expression.fromstring(r"all x.(man(x) -> walks(x))") + p2 = Expression.fromstring(r"man(John)") + c = Expression.fromstring(r"some y.walks(y)") + print(f"{p1}, {p2} |- {c}: {ResolutionProver().prove(c, [p1, p2])}") + + p = Expression.fromstring(r"some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))") + c = Expression.fromstring(r"some e0.walk(e0,mary)") + print(f"{p} |- {c}: {ResolutionProver().prove(c, [p])}") + + +def resolution_test(e): + f = Expression.fromstring(e) + t = ResolutionProver().prove(f) + print(f"|- {f}: {t}") + + +def test_clausify(): + lexpr = Expression.fromstring + + print(clausify(lexpr("P(x) | Q(x)"))) + print(clausify(lexpr("(P(x) & Q(x)) | R(x)"))) + print(clausify(lexpr("P(x) | (Q(x) & R(x))"))) + print(clausify(lexpr("(P(x) & Q(x)) | (R(x) & S(x))"))) + + print(clausify(lexpr("P(x) | Q(x) | R(x)"))) + print(clausify(lexpr("P(x) | (Q(x) & R(x)) | S(x)"))) + + print(clausify(lexpr("exists x.P(x) | Q(x)"))) + + print(clausify(lexpr("-(-P(x) & Q(x))"))) + print(clausify(lexpr("P(x) <-> Q(x)"))) + print(clausify(lexpr("-(P(x) <-> Q(x))"))) + print(clausify(lexpr("-(all x.P(x))"))) + print(clausify(lexpr("-(some x.P(x))"))) + + print(clausify(lexpr("some x.P(x)"))) + print(clausify(lexpr("some x.all y.P(x,y)"))) + print(clausify(lexpr("all y.some x.P(x,y)"))) + print(clausify(lexpr("all z.all y.some x.P(x,y,z)"))) + print(clausify(lexpr("all x.(all y.P(x,y) -> -all y.(Q(x,y) -> R(x,y)))"))) + + +def demo(): + test_clausify() + print() + testResolutionProver() + print() + + p = Expression.fromstring("man(x)") + print(ResolutionProverCommand(p, [p]).prove()) + + +if __name__ == "__main__": + demo() diff --git a/nltk/inference/tableau.py b/nltk/inference/tableau.py new file mode 100644 index 0000000..1e5cd2c --- /dev/null +++ b/nltk/inference/tableau.py @@ -0,0 +1,749 @@ +# Natural Language Toolkit: First-Order Tableau Theorem Prover +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Dan Garrette +# +# URL: +# For license information, see LICENSE.TXT + +""" +Module for a tableau-based First Order theorem prover. +""" + +import time + +from nltk.inference.api import BaseProverCommand, Prover +from nltk.internals import Counter +from nltk.sem.logic import ( + AbstractVariableExpression, + AllExpression, + AndExpression, + ApplicationExpression, + EqualityExpression, + ExistsExpression, + Expression, + FunctionVariableExpression, + IffExpression, + ImpExpression, + LambdaExpression, + NegatedExpression, + OrExpression, + Variable, + VariableExpression, + unique_variable, +) + +_counter = Counter() + + +class ProverParseError(Exception): + pass + + +class TableauProver(Prover): + _assume_false = False + #: Wall-clock limit, in seconds, on a single proof search. A branching + #: tableau can fan out into exponentially many open branches while each stays + #: shallow, so the depth bound below is not enough on its own to keep a + #: crafted formula from pinning a CPU core (CWE-400). Mirroring + #: :class:`nltk.inference.Prover9`'s ``timeout``, the search is abandoned and + #: the goal reported unproved once this many seconds elapse; set it to ``0`` + #: to disable the limit (the original, unbounded behaviour). + TIMEOUT = 60 + #: Deadline (``time.monotonic`` value) for the current proof; set per call in + #: :meth:`_prove`. ``None`` means no wall-clock limit is in force. + _deadline = None + #: Upper bound on the tableau expansion depth (``debug.indent``). The prover + #: recurses once per expansion step, so a formula that generates an infinite + #: tableau (e.g. ``all x.exists y.succ(x,y)``) recurses without limit until it + #: raises an uncaught ``RecursionError`` (crashing the caller) or, with a + #: larger stack, hangs (CWE-674 / CWE-770). Stopping at this depth treats the + #: branch as not closed (the proof is reported unproved) and keeps the + #: recursion well below Python's limit; raise it if you need a deeper proof. + MAX_TABLEAU_DEPTH = 200 + + def _prove(self, goal=None, assumptions=None, verbose=False): + if not assumptions: + assumptions = [] + + result = None + try: + agenda = Agenda() + if goal: + agenda.put(-goal) + agenda.put_all(assumptions) + debugger = Debug(verbose) + self._deadline = time.monotonic() + self.TIMEOUT if self.TIMEOUT else None + result = self._attempt_proof(agenda, set(), set(), debugger) + except RuntimeError as e: + if self._assume_false and str(e).startswith( + "maximum recursion depth exceeded" + ): + result = False + else: + if verbose: + print(e) + else: + raise e + return (result, "\n".join(debugger.lines)) + + def _attempt_proof(self, agenda, accessible_vars, atoms, debug): + # Bound the expansion depth so an infinite tableau can't recurse into an + # uncaught RecursionError (crashing the caller) or hang (CWE-674/CWE-770). + # ``debug.indent`` is incremented once per recursion, so it tracks depth. + # Stopping here leaves the branch unclosed, i.e. reports the goal unproved. + if debug.indent > self.MAX_TABLEAU_DEPTH: + debug.line("MAX DEPTH REACHED") + return False + + # Bound the total search by wall-clock time as well: a branching tableau + # can fan out without growing deep, so the depth bound alone does not + # stop it pinning a CPU core (CWE-400). This runs once per expanded node. + if self._deadline is not None and time.monotonic() > self._deadline: + debug.line("TIMEOUT REACHED") + return False + + (current, context), category = agenda.pop_first() + + # if there's nothing left in the agenda, and we haven't closed the path + if not current: + debug.line("AGENDA EMPTY") + return False + + proof_method = { + Categories.ATOM: self._attempt_proof_atom, + Categories.PROP: self._attempt_proof_prop, + Categories.N_ATOM: self._attempt_proof_n_atom, + Categories.N_PROP: self._attempt_proof_n_prop, + Categories.APP: self._attempt_proof_app, + Categories.N_APP: self._attempt_proof_n_app, + Categories.N_EQ: self._attempt_proof_n_eq, + Categories.D_NEG: self._attempt_proof_d_neg, + Categories.N_ALL: self._attempt_proof_n_all, + Categories.N_EXISTS: self._attempt_proof_n_some, + Categories.AND: self._attempt_proof_and, + Categories.N_OR: self._attempt_proof_n_or, + Categories.N_IMP: self._attempt_proof_n_imp, + Categories.OR: self._attempt_proof_or, + Categories.IMP: self._attempt_proof_imp, + Categories.N_AND: self._attempt_proof_n_and, + Categories.IFF: self._attempt_proof_iff, + Categories.N_IFF: self._attempt_proof_n_iff, + Categories.EQ: self._attempt_proof_eq, + Categories.EXISTS: self._attempt_proof_some, + Categories.ALL: self._attempt_proof_all, + }[category] + + debug.line((current, context)) + return proof_method(current, context, agenda, accessible_vars, atoms, debug) + + def _attempt_proof_atom( + self, current, context, agenda, accessible_vars, atoms, debug + ): + # Check if the branch is closed. Return 'True' if it is + if (current, True) in atoms: + debug.line("CLOSED", 1) + return True + + if context: + if isinstance(context.term, NegatedExpression): + current = current.negate() + agenda.put(context(current).simplify()) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + else: + # mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars + agenda.mark_alls_fresh() + return self._attempt_proof( + agenda, + accessible_vars | set(current.args), + atoms | {(current, False)}, + debug + 1, + ) + + def _attempt_proof_n_atom( + self, current, context, agenda, accessible_vars, atoms, debug + ): + # Check if the branch is closed. Return 'True' if it is + if (current.term, False) in atoms: + debug.line("CLOSED", 1) + return True + + if context: + if isinstance(context.term, NegatedExpression): + current = current.negate() + agenda.put(context(current).simplify()) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + else: + # mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars + agenda.mark_alls_fresh() + return self._attempt_proof( + agenda, + accessible_vars | set(current.term.args), + atoms | {(current.term, True)}, + debug + 1, + ) + + def _attempt_proof_prop( + self, current, context, agenda, accessible_vars, atoms, debug + ): + # Check if the branch is closed. Return 'True' if it is + if (current, True) in atoms: + debug.line("CLOSED", 1) + return True + + # mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars + agenda.mark_alls_fresh() + return self._attempt_proof( + agenda, accessible_vars, atoms | {(current, False)}, debug + 1 + ) + + def _attempt_proof_n_prop( + self, current, context, agenda, accessible_vars, atoms, debug + ): + # Check if the branch is closed. Return 'True' if it is + if (current.term, False) in atoms: + debug.line("CLOSED", 1) + return True + + # mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars + agenda.mark_alls_fresh() + return self._attempt_proof( + agenda, accessible_vars, atoms | {(current.term, True)}, debug + 1 + ) + + def _attempt_proof_app( + self, current, context, agenda, accessible_vars, atoms, debug + ): + f, args = current.uncurry() + for i, arg in enumerate(args): + if not TableauProver.is_atom(arg): + ctx = f + nv = Variable("X%s" % _counter.get()) + for j, a in enumerate(args): + ctx = ctx(VariableExpression(nv)) if i == j else ctx(a) + if context: + ctx = context(ctx).simplify() + ctx = LambdaExpression(nv, ctx) + agenda.put(arg, ctx) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + raise Exception("If this method is called, there must be a non-atomic argument") + + def _attempt_proof_n_app( + self, current, context, agenda, accessible_vars, atoms, debug + ): + f, args = current.term.uncurry() + for i, arg in enumerate(args): + if not TableauProver.is_atom(arg): + ctx = f + nv = Variable("X%s" % _counter.get()) + for j, a in enumerate(args): + ctx = ctx(VariableExpression(nv)) if i == j else ctx(a) + if context: + # combine new context with existing + ctx = context(ctx).simplify() + ctx = LambdaExpression(nv, -ctx) + agenda.put(-arg, ctx) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + raise Exception("If this method is called, there must be a non-atomic argument") + + def _attempt_proof_n_eq( + self, current, context, agenda, accessible_vars, atoms, debug + ): + ########################################################################### + # Since 'current' is of type '~(a=b)', the path is closed if 'a' == 'b' + ########################################################################### + if current.term.first == current.term.second: + debug.line("CLOSED", 1) + return True + + agenda[Categories.N_EQ].add((current, context)) + current._exhausted = True + return self._attempt_proof( + agenda, + accessible_vars | {current.term.first, current.term.second}, + atoms, + debug + 1, + ) + + def _attempt_proof_d_neg( + self, current, context, agenda, accessible_vars, atoms, debug + ): + agenda.put(current.term.term, context) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_n_all( + self, current, context, agenda, accessible_vars, atoms, debug + ): + agenda[Categories.EXISTS].add( + (ExistsExpression(current.term.variable, -current.term.term), context) + ) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_n_some( + self, current, context, agenda, accessible_vars, atoms, debug + ): + agenda[Categories.ALL].add( + (AllExpression(current.term.variable, -current.term.term), context) + ) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_and( + self, current, context, agenda, accessible_vars, atoms, debug + ): + agenda.put(current.first, context) + agenda.put(current.second, context) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_n_or( + self, current, context, agenda, accessible_vars, atoms, debug + ): + agenda.put(-current.term.first, context) + agenda.put(-current.term.second, context) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_n_imp( + self, current, context, agenda, accessible_vars, atoms, debug + ): + agenda.put(current.term.first, context) + agenda.put(-current.term.second, context) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_or( + self, current, context, agenda, accessible_vars, atoms, debug + ): + new_agenda = agenda.clone() + agenda.put(current.first, context) + new_agenda.put(current.second, context) + return self._attempt_proof( + agenda, accessible_vars, atoms, debug + 1 + ) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_imp( + self, current, context, agenda, accessible_vars, atoms, debug + ): + new_agenda = agenda.clone() + agenda.put(-current.first, context) + new_agenda.put(current.second, context) + return self._attempt_proof( + agenda, accessible_vars, atoms, debug + 1 + ) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_n_and( + self, current, context, agenda, accessible_vars, atoms, debug + ): + new_agenda = agenda.clone() + agenda.put(-current.term.first, context) + new_agenda.put(-current.term.second, context) + return self._attempt_proof( + agenda, accessible_vars, atoms, debug + 1 + ) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_iff( + self, current, context, agenda, accessible_vars, atoms, debug + ): + new_agenda = agenda.clone() + agenda.put(current.first, context) + agenda.put(current.second, context) + new_agenda.put(-current.first, context) + new_agenda.put(-current.second, context) + return self._attempt_proof( + agenda, accessible_vars, atoms, debug + 1 + ) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_n_iff( + self, current, context, agenda, accessible_vars, atoms, debug + ): + new_agenda = agenda.clone() + agenda.put(current.term.first, context) + agenda.put(-current.term.second, context) + new_agenda.put(-current.term.first, context) + new_agenda.put(current.term.second, context) + return self._attempt_proof( + agenda, accessible_vars, atoms, debug + 1 + ) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1) + + def _attempt_proof_eq( + self, current, context, agenda, accessible_vars, atoms, debug + ): + ######################################################################### + # Since 'current' is of the form '(a = b)', replace ALL free instances + # of 'a' with 'b' + ######################################################################### + agenda.put_atoms(atoms) + agenda.replace_all(current.first, current.second) + accessible_vars.discard(current.first) + agenda.mark_neqs_fresh() + return self._attempt_proof(agenda, accessible_vars, set(), debug + 1) + + def _attempt_proof_some( + self, current, context, agenda, accessible_vars, atoms, debug + ): + new_unique_variable = VariableExpression(unique_variable()) + agenda.put(current.term.replace(current.variable, new_unique_variable), context) + agenda.mark_alls_fresh() + return self._attempt_proof( + agenda, accessible_vars | {new_unique_variable}, atoms, debug + 1 + ) + + def _attempt_proof_all( + self, current, context, agenda, accessible_vars, atoms, debug + ): + try: + current._used_vars + except AttributeError: + current._used_vars = set() + + # if there are accessible_vars on the path + if accessible_vars: + # get the set of bound variables that have not be used by this AllExpression + bv_available = accessible_vars - current._used_vars + + if bv_available: + variable_to_use = list(bv_available)[0] + debug.line("--> Using '%s'" % variable_to_use, 2) + current._used_vars |= {variable_to_use} + agenda.put( + current.term.replace(current.variable, variable_to_use), context + ) + agenda[Categories.ALL].add((current, context)) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + else: + # no more available variables to substitute + debug.line("--> Variables Exhausted", 2) + current._exhausted = True + agenda[Categories.ALL].add((current, context)) + return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1) + + else: + new_unique_variable = VariableExpression(unique_variable()) + debug.line("--> Using '%s'" % new_unique_variable, 2) + current._used_vars |= {new_unique_variable} + agenda.put( + current.term.replace(current.variable, new_unique_variable), context + ) + agenda[Categories.ALL].add((current, context)) + agenda.mark_alls_fresh() + return self._attempt_proof( + agenda, accessible_vars | {new_unique_variable}, atoms, debug + 1 + ) + + @staticmethod + def is_atom(e): + if isinstance(e, NegatedExpression): + e = e.term + + if isinstance(e, ApplicationExpression): + for arg in e.args: + if not TableauProver.is_atom(arg): + return False + return True + elif isinstance(e, AbstractVariableExpression) or isinstance( + e, LambdaExpression + ): + return True + else: + return False + + +class TableauProverCommand(BaseProverCommand): + def __init__(self, goal=None, assumptions=None, prover=None): + """ + :param goal: Input expression to prove + :type goal: sem.Expression + :param assumptions: Input expressions to use as assumptions in + the proof. + :type assumptions: list(sem.Expression) + """ + if prover is not None: + assert isinstance(prover, TableauProver) + else: + prover = TableauProver() + + BaseProverCommand.__init__(self, prover, goal, assumptions) + + +class Agenda: + def __init__(self): + self.sets = tuple(set() for i in range(21)) + + def clone(self): + new_agenda = Agenda() + set_list = [s.copy() for s in self.sets] + + new_allExs = set() + for allEx, _ in set_list[Categories.ALL]: + new_allEx = AllExpression(allEx.variable, allEx.term) + try: + new_allEx._used_vars = {used for used in allEx._used_vars} + except AttributeError: + new_allEx._used_vars = set() + new_allExs.add((new_allEx, None)) + set_list[Categories.ALL] = new_allExs + + set_list[Categories.N_EQ] = { + (NegatedExpression(n_eq.term), ctx) + for (n_eq, ctx) in set_list[Categories.N_EQ] + } + + new_agenda.sets = tuple(set_list) + return new_agenda + + def __getitem__(self, index): + return self.sets[index] + + def put(self, expression, context=None): + if isinstance(expression, AllExpression): + ex_to_add = AllExpression(expression.variable, expression.term) + try: + ex_to_add._used_vars = {used for used in expression._used_vars} + except AttributeError: + ex_to_add._used_vars = set() + else: + ex_to_add = expression + self.sets[self._categorize_expression(ex_to_add)].add((ex_to_add, context)) + + def put_all(self, expressions): + for expression in expressions: + self.put(expression) + + def put_atoms(self, atoms): + for atom, neg in atoms: + if neg: + self[Categories.N_ATOM].add((-atom, None)) + else: + self[Categories.ATOM].add((atom, None)) + + def pop_first(self): + """Pop the first expression that appears in the agenda""" + for i, s in enumerate(self.sets): + if s: + if i in [Categories.N_EQ, Categories.ALL]: + for ex in s: + try: + if not ex[0]._exhausted: + s.remove(ex) + return (ex, i) + except AttributeError: + s.remove(ex) + return (ex, i) + else: + return (s.pop(), i) + return ((None, None), None) + + def replace_all(self, old, new): + for s in self.sets: + for ex, ctx in s: + ex.replace(old.variable, new) + if ctx is not None: + ctx.replace(old.variable, new) + + def mark_alls_fresh(self): + for u, _ in self.sets[Categories.ALL]: + u._exhausted = False + + def mark_neqs_fresh(self): + for neq, _ in self.sets[Categories.N_EQ]: + neq._exhausted = False + + def _categorize_expression(self, current): + if isinstance(current, NegatedExpression): + return self._categorize_NegatedExpression(current) + elif isinstance(current, FunctionVariableExpression): + return Categories.PROP + elif TableauProver.is_atom(current): + return Categories.ATOM + elif isinstance(current, AllExpression): + return Categories.ALL + elif isinstance(current, AndExpression): + return Categories.AND + elif isinstance(current, OrExpression): + return Categories.OR + elif isinstance(current, ImpExpression): + return Categories.IMP + elif isinstance(current, IffExpression): + return Categories.IFF + elif isinstance(current, EqualityExpression): + return Categories.EQ + elif isinstance(current, ExistsExpression): + return Categories.EXISTS + elif isinstance(current, ApplicationExpression): + return Categories.APP + else: + raise ProverParseError("cannot categorize %s" % current.__class__.__name__) + + def _categorize_NegatedExpression(self, current): + negated = current.term + + if isinstance(negated, NegatedExpression): + return Categories.D_NEG + elif isinstance(negated, FunctionVariableExpression): + return Categories.N_PROP + elif TableauProver.is_atom(negated): + return Categories.N_ATOM + elif isinstance(negated, AllExpression): + return Categories.N_ALL + elif isinstance(negated, AndExpression): + return Categories.N_AND + elif isinstance(negated, OrExpression): + return Categories.N_OR + elif isinstance(negated, ImpExpression): + return Categories.N_IMP + elif isinstance(negated, IffExpression): + return Categories.N_IFF + elif isinstance(negated, EqualityExpression): + return Categories.N_EQ + elif isinstance(negated, ExistsExpression): + return Categories.N_EXISTS + elif isinstance(negated, ApplicationExpression): + return Categories.N_APP + else: + raise ProverParseError("cannot categorize %s" % negated.__class__.__name__) + + +class Debug: + def __init__(self, verbose, indent=0, lines=None): + self.verbose = verbose + self.indent = indent + + if not lines: + lines = [] + self.lines = lines + + def __add__(self, increment): + return Debug(self.verbose, self.indent + 1, self.lines) + + def line(self, data, indent=0): + if isinstance(data, tuple): + ex, ctx = data + if ctx: + data = f"{ex}, {ctx}" + else: + data = "%s" % ex + + if isinstance(ex, AllExpression): + try: + used_vars = "[%s]" % ( + ",".join("%s" % ve.variable.name for ve in ex._used_vars) + ) + data += ": %s" % used_vars + except AttributeError: + data += ": []" + + newline = "{}{}".format(" " * (self.indent + indent), data) + self.lines.append(newline) + + if self.verbose: + print(newline) + + +class Categories: + ATOM = 0 + PROP = 1 + N_ATOM = 2 + N_PROP = 3 + APP = 4 + N_APP = 5 + N_EQ = 6 + D_NEG = 7 + N_ALL = 8 + N_EXISTS = 9 + AND = 10 + N_OR = 11 + N_IMP = 12 + OR = 13 + IMP = 14 + N_AND = 15 + IFF = 16 + N_IFF = 17 + EQ = 18 + EXISTS = 19 + ALL = 20 + + +def testTableauProver(): + tableau_test("P | -P") + tableau_test("P & -P") + tableau_test("Q", ["P", "(P -> Q)"]) + tableau_test("man(x)") + tableau_test("(man(x) -> man(x))") + tableau_test("(man(x) -> --man(x))") + tableau_test("-(man(x) and -man(x))") + tableau_test("(man(x) or -man(x))") + tableau_test("(man(x) -> man(x))") + tableau_test("-(man(x) and -man(x))") + tableau_test("(man(x) or -man(x))") + tableau_test("(man(x) -> man(x))") + tableau_test("(man(x) iff man(x))") + tableau_test("-(man(x) iff -man(x))") + tableau_test("all x.man(x)") + tableau_test("all x.all y.((x = y) -> (y = x))") + tableau_test("all x.all y.all z.(((x = y) & (y = z)) -> (x = z))") + # tableau_test('-all x.some y.F(x,y) & some x.all y.(-F(x,y))') + # tableau_test('some x.all y.sees(x,y)') + + p1 = "all x.(man(x) -> mortal(x))" + p2 = "man(Socrates)" + c = "mortal(Socrates)" + tableau_test(c, [p1, p2]) + + p1 = "all x.(man(x) -> walks(x))" + p2 = "man(John)" + c = "some y.walks(y)" + tableau_test(c, [p1, p2]) + + p = "((x = y) & walks(y))" + c = "walks(x)" + tableau_test(c, [p]) + + p = "((x = y) & ((y = z) & (z = w)))" + c = "(x = w)" + tableau_test(c, [p]) + + p = "some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))" + c = "some e0.walk(e0,mary)" + tableau_test(c, [p]) + + c = "(exists x.exists z3.((x = Mary) & ((z3 = John) & sees(z3,x))) <-> exists x.exists z4.((x = John) & ((z4 = Mary) & sees(x,z4))))" + tableau_test(c) + + +# p = 'some e1.some e2.((believe e1 john e2) and (walk e2 mary))' +# c = 'some x.some e3.some e4.((believe e3 x e4) and (walk e4 mary))' +# tableau_test(c, [p]) + + +def testHigherOrderTableauProver(): + tableau_test("believe(j, -lie(b))", ["believe(j, -lie(b) & -cheat(b))"]) + tableau_test("believe(j, lie(b) & cheat(b))", ["believe(j, lie(b))"]) + tableau_test( + "believe(j, lie(b))", ["lie(b)"] + ) # how do we capture that John believes all things that are true + tableau_test( + "believe(j, know(b, cheat(b)))", + ["believe(j, know(b, lie(b)) & know(b, steals(b) & cheat(b)))"], + ) + tableau_test("P(Q(y), R(y) & R(z))", ["P(Q(x) & Q(y), R(y) & R(z))"]) + + tableau_test("believe(j, cheat(b) & lie(b))", ["believe(j, lie(b) & cheat(b))"]) + tableau_test("believe(j, -cheat(b) & -lie(b))", ["believe(j, -lie(b) & -cheat(b))"]) + + +def tableau_test(c, ps=None, verbose=False): + pc = Expression.fromstring(c) + pps = [Expression.fromstring(p) for p in ps] if ps else [] + if not ps: + ps = [] + print( + "%s |- %s: %s" + % (", ".join(ps), pc, TableauProver().prove(pc, pps, verbose=verbose)) + ) + + +def demo(): + testTableauProver() + testHigherOrderTableauProver() + + +if __name__ == "__main__": + demo() diff --git a/nltk/internals.py b/nltk/internals.py new file mode 100644 index 0000000..61c6110 --- /dev/null +++ b/nltk/internals.py @@ -0,0 +1,1233 @@ +# Natural Language Toolkit: Internal utility functions +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# Nitin Madnani +# URL: +# For license information, see LICENSE.TXT + +import fnmatch +import locale +import os +import re +import stat +import subprocess +import sys +import textwrap +import types +import warnings +from xml.etree import ElementTree + +########################################################################## +# Java Via Command-Line +########################################################################## + +_java_bin = None +_java_options = [] + +# Allowlist of safe JVM tuning flags for NLTK's Java wrapper. +# Anything not matching is rejected to prevent argument injection +# (CVE-2026-12841, CWE-88). An allowlist is used rather than a +# denylist so that -jar, @argfile, and future dangerous flags are +# blocked without needing to be enumerated explicitly. +_SAFE_JVM_PREFIXES = ( + "-xmx", # max heap size: -Xmx512m + "-mx", # max heap (legacy alias of -Xmx, used by Stanford/CoreNLP): -mx2g + "-xms", # initial heap: -Xms128m + "-ms", # initial heap (legacy alias of -Xms): -ms128m + "-xss", # thread stack: -Xss4m + "-ss", # thread stack (legacy alias of -Xss): -ss4m + "-xbatch", # disable bg JIT + "-xint", # interpret-only mode + "-xcomp", # compile-only mode + "-xmixed", # mixed mode (JVM default) + "-verbose", # diagnostic output: -verbose:gc + "-xx:", # advanced tuning: -XX:+UseG1GC +) + +_SAFE_JVM_EXACT = frozenset({"-server", "-client"}) + + +def _validate_java_options(options): + """ + Raise ValueError if *options* contains JVM flags that can change + the executed program, load agents, or expand argument files. + + Uses an allowlist of safe JVM memory/tuning flags that NLTK's Java + wrapper is known to need. This is intentionally stricter than a + denylist so that -jar, @argfile, and future dangerous flags are + rejected without needing to be enumerated (CVE-2026-12841, CWE-88). + """ + for flag in options: + n = flag.lower() + + # @argfile references are expanded by the Java launcher before + # any other argument processing and can smuggle blocked flags. + if n.startswith("@"): + raise ValueError( + f"java_options contains a disallowed Java argument file " + f"reference: {flag!r} (CVE-2026-12841, CWE-88)." + ) + + # Allow -Dkey=value system properties. The prefix is always + # uppercase -D in valid usage; check the original flag. + if flag.startswith("-D") and "=" in flag: + continue + + if n in _SAFE_JVM_EXACT: + continue + + if n.startswith(_SAFE_JVM_PREFIXES): + continue + + raise ValueError( + f"java_options contains a disallowed JVM/launcher flag: {flag!r}. " + "Only JVM memory-tuning and safe runtime flags are permitted " + "(CVE-2026-12841, CWE-88)." + ) + + +# [xx] add classpath option to config_java? +def config_java(bin=None, options=None, verbose=False): + """ + Configure nltk's java interface, by letting nltk know where it can + find the Java binary, and what extra options (if any) should be + passed to Java when it is run. + + :param bin: The full path to the Java binary. If not specified, + then nltk will search the system for a Java binary; and if + one is not found, it will raise a ``LookupError`` exception. + :type bin: str + :param options: A list of options that should be passed to the + Java binary when it is called. A common value is + ``'-Xmx512m'``, which tells Java binary to increase + the maximum heap size to 512 megabytes. If no options are + specified, then do not modify the options list. + :type options: list(str) + """ + global _java_bin, _java_options + _java_bin = find_binary( + "java", + bin, + env_vars=["JAVAHOME", "JAVA_HOME"], + verbose=verbose, + binary_names=["java.exe"], + ) + + if options is not None: + if isinstance(options, str): + options = options.split() + options = list(options) + _validate_java_options(options) + _java_options[:] = options + + +def java( + cmd, + classpath=None, + stdin=None, + stdout=None, + stderr=None, + blocking=True, + *, + options=None, +): + """ + Execute the given java command, by opening a subprocess that calls + Java. If java has not yet been configured, it will be configured + by calling ``config_java()`` with no arguments. + + :param cmd: The java command that should be called, formatted as + a list of strings. Typically, the first string will be the name + of the java class; and the remaining strings will be arguments + for that java class. + :type cmd: list(str) + + :param classpath: A ``':'`` separated list of directories, JAR + archives, and ZIP archives to search for class files. + :type classpath: str + + :param stdin: Specify the executed program's + standard input file handles, respectively. Valid values are ``subprocess.PIPE``, + an existing file descriptor (a positive integer), an existing + file object, 'pipe', 'stdout', 'devnull' and None. ``subprocess.PIPE`` indicates that a + new pipe to the child should be created. With None, no + redirection will occur; the child's file handles will be + inherited from the parent. Additionally, stderr can be + ``subprocess.STDOUT``, which indicates that the stderr data + from the applications should be captured into the same file + handle as for stdout. + + :param stdout: Specify the executed program's standard output file + handle. See ``stdin`` for valid values. + + :param stderr: Specify the executed program's standard error file + handle. See ``stdin`` for valid values. + + + :param blocking: If ``false``, then return immediately after + spawning the subprocess. In this case, the return value is + the ``Popen`` object, and not a ``(stdout, stderr)`` tuple. + :param options: Java options to use for this subprocess call. If not + specified, use the global options configured by ``config_java()``. + + :return: If ``blocking=True``, then return a tuple ``(stdout, + stderr)``, containing the stdout and stderr outputs generated + by the java command if the ``stdout`` and ``stderr`` parameters + were set to ``subprocess.PIPE``; or None otherwise. If + ``blocking=False``, then return a ``subprocess.Popen`` object. + + :raise OSError: If the java command returns a nonzero return code. + """ + + subprocess_output_dict = { + "pipe": subprocess.PIPE, + "stdout": subprocess.STDOUT, + "devnull": subprocess.DEVNULL, + } + + stdin = subprocess_output_dict.get(stdin, stdin) + stdout = subprocess_output_dict.get(stdout, stdout) + stderr = subprocess_output_dict.get(stderr, stderr) + + if isinstance(cmd, str): + raise TypeError("cmd should be a list of strings") + + # Make sure we know where a java binary is. + if _java_bin is None: + config_java() + + # Set up the classpath. + if isinstance(classpath, str): + classpaths = [classpath] + else: + classpaths = list(classpath) + classpath = os.path.pathsep.join(classpaths) + + # Construct the full command string. + cmd = list(cmd) + cmd = ["-cp", classpath] + cmd + if options is None: + java_options = _java_options + else: + if isinstance(options, str): + options = options.split() + java_options = list(options) + cmd = [_java_bin] + java_options + cmd + + # Call java via a subprocess + p = subprocess.Popen(cmd, stdin=stdin, stdout=stdout, stderr=stderr) + if not blocking: + return p + stdout, stderr = p.communicate() + + # Check the return code. + if p.returncode != 0: + print(_decode_stdoutdata(stderr)) + raise OSError("Java command failed : " + str(cmd)) + + return (stdout, stderr) + + +###################################################################### +# Parsing +###################################################################### + + +class ReadError(ValueError): + """ + Exception raised by read_* functions when they fail. + :param position: The index in the input string where an error occurred. + :param expected: What was expected when an error occurred. + """ + + def __init__(self, expected, position): + ValueError.__init__(self, expected, position) + self.expected = expected + self.position = position + + def __str__(self): + return f"Expected {self.expected} at {self.position}" + + +_STRING_START_RE = re.compile(r"[uU]?[rR]?(\"\"\"|\'\'\'|\"|\')") + + +def read_str(s, start_position): + """ + If a Python string literal begins at the specified position in the + given string, then return a tuple ``(val, end_position)`` + containing the value of the string literal and the position where + it ends. Otherwise, raise a ``ReadError``. + + :param s: A string that will be checked to see if within which a + Python string literal exists. + :type s: str + + :param start_position: The specified beginning position of the string ``s`` + to begin regex matching. + :type start_position: int + + :return: A tuple containing the matched string literal evaluated as a + string and the end position of the string literal. + :rtype: tuple(str, int) + + :raise ReadError: If the ``_STRING_START_RE`` regex doesn't return a + match in ``s`` at ``start_position``, i.e., open quote. If the + ``_STRING_END_RE`` regex doesn't return a match in ``s`` at the + end of the first match, i.e., close quote. + :raise ValueError: If an invalid string (i.e., contains an invalid + escape sequence) is passed into the ``eval``. + + :Example: + + >>> from nltk.internals import read_str + >>> read_str('"Hello", World!', 0) + ('Hello', 7) + + """ + # Read the open quote, and any modifiers. + m = _STRING_START_RE.match(s, start_position) + if not m: + raise ReadError("open quote", start_position) + quotemark = m.group(1) + + # Find the close quote. + _STRING_END_RE = re.compile(r"\\|%s" % quotemark) + position = m.end() + while True: + match = _STRING_END_RE.search(s, position) + if not match: + raise ReadError("close quote", position) + if match.group(0) == "\\": + position = match.end() + 1 + else: + break + + # Process it, using eval. Strings with invalid escape sequences + # might raise ValueError. + try: + return eval(s[start_position : match.end()]), match.end() + except ValueError as e: + raise ReadError("valid escape sequence", start_position) from e + + +_READ_INT_RE = re.compile(r"-?\d+") + + +def read_int(s, start_position): + """ + If an integer begins at the specified position in the given + string, then return a tuple ``(val, end_position)`` containing the + value of the integer and the position where it ends. Otherwise, + raise a ``ReadError``. + + :param s: A string that will be checked to see if within which a + Python integer exists. + :type s: str + + :param start_position: The specified beginning position of the string ``s`` + to begin regex matching. + :type start_position: int + + :return: A tuple containing the matched integer casted to an int, + and the end position of the int in ``s``. + :rtype: tuple(int, int) + + :raise ReadError: If the ``_READ_INT_RE`` regex doesn't return a + match in ``s`` at ``start_position``. + + :Example: + + >>> from nltk.internals import read_int + >>> read_int('42 is the answer', 0) + (42, 2) + + """ + m = _READ_INT_RE.match(s, start_position) + if not m: + raise ReadError("integer", start_position) + return int(m.group()), m.end() + + +_READ_NUMBER_VALUE = re.compile(r"-?(\d*)([.]?\d*)?") + + +def read_number(s, start_position): + """ + If an integer or float begins at the specified position in the + given string, then return a tuple ``(val, end_position)`` + containing the value of the number and the position where it ends. + Otherwise, raise a ``ReadError``. + + :param s: A string that will be checked to see if within which a + Python number exists. + :type s: str + + :param start_position: The specified beginning position of the string ``s`` + to begin regex matching. + :type start_position: int + + :return: A tuple containing the matched number casted to a ``float``, + and the end position of the number in ``s``. + :rtype: tuple(float, int) + + :raise ReadError: If the ``_READ_NUMBER_VALUE`` regex doesn't return a + match in ``s`` at ``start_position``. + + :Example: + + >>> from nltk.internals import read_number + >>> read_number('Pi is 3.14159', 6) + (3.14159, 13) + + """ + m = _READ_NUMBER_VALUE.match(s, start_position) + if not m or not (m.group(1) or m.group(2)): + raise ReadError("number", start_position) + if m.group(2): + return float(m.group()), m.end() + else: + return int(m.group()), m.end() + + +###################################################################### +# Check if a method has been overridden +###################################################################### + + +def overridden(method): + """ + :return: True if ``method`` overrides some method with the same + name in a base class. This is typically used when defining + abstract base classes or interfaces, to allow subclasses to define + either of two related methods: + + >>> class EaterI: + ... '''Subclass must define eat() or batch_eat().''' + ... def eat(self, food): + ... if overridden(self.batch_eat): + ... return self.batch_eat([food])[0] + ... else: + ... raise NotImplementedError() + ... def batch_eat(self, foods): + ... return [self.eat(food) for food in foods] + + :type method: instance method + """ + if isinstance(method, types.MethodType) and method.__self__.__class__ is not None: + name = method.__name__ + funcs = [ + cls.__dict__[name] + for cls in _mro(method.__self__.__class__) + if name in cls.__dict__ + ] + return len(funcs) > 1 + else: + raise TypeError("Expected an instance method.") + + +def _mro(cls): + """ + Return the method resolution order for ``cls`` -- i.e., a list + containing ``cls`` and all its base classes, in the order in which + they would be checked by ``getattr``. For new-style classes, this + is just cls.__mro__. For classic classes, this can be obtained by + a depth-first left-to-right traversal of ``__bases__``. + """ + if isinstance(cls, type): + return cls.__mro__ + else: + mro = [cls] + for base in cls.__bases__: + mro.extend(_mro(base)) + return mro + + +###################################################################### +# Deprecation decorator & base class +###################################################################### +# [xx] dedent msg first if it comes from a docstring. + + +def _add_epytext_field(obj, field, message): + """Add an epytext @field to a given object's docstring.""" + indent = "" + # If we already have a docstring, then add a blank line to separate + # it from the new field, and check its indentation. + if obj.__doc__: + obj.__doc__ = obj.__doc__.rstrip() + "\n\n" + indents = re.findall(r"(?<=\n)[ ]+(?!\s)", obj.__doc__.expandtabs()) + if indents: + indent = min(indents) + # If we don't have a docstring, add an empty one. + else: + obj.__doc__ = "" + + obj.__doc__ += textwrap.fill( + f"@{field}: {message}", + initial_indent=indent, + subsequent_indent=indent + " ", + ) + + +def deprecated(message): + """ + A decorator used to mark functions as deprecated. This will cause + a warning to be printed the when the function is used. Usage: + + >>> from nltk.internals import deprecated + >>> @deprecated('Use foo() instead') + ... def bar(x): + ... print(x/10) + + """ + + def decorator(func): + msg = f"Function {func.__name__}() has been deprecated. {message}" + msg = "\n" + textwrap.fill(msg, initial_indent=" ", subsequent_indent=" ") + + def newFunc(*args, **kwargs): + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + + # Copy the old function's name, docstring, & dict + newFunc.__dict__.update(func.__dict__) + newFunc.__name__ = func.__name__ + newFunc.__doc__ = func.__doc__ + newFunc.__deprecated__ = True + # Add a @deprecated field to the docstring. + _add_epytext_field(newFunc, "deprecated", message) + return newFunc + + return decorator + + +class Deprecated: + """ + A base class used to mark deprecated classes. A typical usage is to + alert users that the name of a class has changed: + + >>> from nltk.internals import Deprecated + >>> class NewClassName: + ... pass # All logic goes here. + ... + >>> class OldClassName(Deprecated, NewClassName): + ... "Use NewClassName instead." + + The docstring of the deprecated class will be used in the + deprecation warning message. + """ + + def __new__(cls, *args, **kwargs): + # Figure out which class is the deprecated one. + dep_cls = None + for base in _mro(cls): + if Deprecated in base.__bases__: + dep_cls = base + break + assert dep_cls, "Unable to determine which base is deprecated." + + # Construct an appropriate warning. + doc = dep_cls.__doc__ or "".strip() + # If there's a @deprecated field, strip off the field marker. + doc = re.sub(r"\A\s*@deprecated:", r"", doc) + # Strip off any indentation. + doc = re.sub(r"(?m)^\s*", "", doc) + # Construct a 'name' string. + name = "Class %s" % dep_cls.__name__ + if cls != dep_cls: + name += " (base class for %s)" % cls.__name__ + # Put it all together. + msg = f"{name} has been deprecated. {doc}" + # Wrap it. + msg = "\n" + textwrap.fill(msg, initial_indent=" ", subsequent_indent=" ") + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + # Do the actual work of __new__. + return object.__new__(cls) + + +########################################################################## +# COUNTER, FOR UNIQUE NAMING +########################################################################## + + +class Counter: + """ + A counter that auto-increments each time its value is read. + """ + + def __init__(self, initial_value=0): + self._value = initial_value + + def get(self): + self._value += 1 + return self._value + + +########################################################################## +# Search for files/binaries +########################################################################## + + +def find_file_iter( + filename, + env_vars=(), + searchpath=(), + file_names=None, + url=None, + verbose=False, + finding_dir=False, +): + """ + Search for a file to be used by nltk. + + :param filename: The name or path of the file. + :param env_vars: A list of environment variable names to check. + :param file_names: A list of alternative file names to check. + :param searchpath: List of directories to search. + :param url: URL presented to user for download help. + :param verbose: Whether or not to print path when a file is found. + """ + file_names = [filename] + (file_names or []) + assert isinstance(filename, str) + assert not isinstance(file_names, str) + assert not isinstance(searchpath, str) + if isinstance(env_vars, str): + env_vars = env_vars.split() + yielded = False + + # File exists, no magic + for alternative in file_names: + path_to_file = os.path.join(filename, alternative) + if os.path.isfile(path_to_file): + if verbose: + print(f"[Found {filename}: {path_to_file}]") + yielded = True + yield path_to_file + # Check the bare alternatives + if os.path.isfile(alternative): + if verbose: + print(f"[Found {filename}: {alternative}]") + yielded = True + yield alternative + # Check if the alternative is inside a 'file' directory + path_to_file = os.path.join(filename, "file", alternative) + if os.path.isfile(path_to_file): + if verbose: + print(f"[Found {filename}: {path_to_file}]") + yielded = True + yield path_to_file + + # Check environment variables + for env_var in env_vars: + if env_var in os.environ: + if finding_dir: # This is to file a directory instead of file + yielded = True + yield os.environ[env_var] + + for env_dir in os.environ[env_var].split(os.pathsep): + # Check if the environment variable contains a direct path to the bin + if os.path.isfile(env_dir): + if verbose: + print(f"[Found {filename}: {env_dir}]") + yielded = True + yield env_dir + # Check if the possible bin names exist inside the environment variable directories + for alternative in file_names: + path_to_file = os.path.join(env_dir, alternative) + if os.path.isfile(path_to_file): + if verbose: + print(f"[Found {filename}: {path_to_file}]") + yielded = True + yield path_to_file + # Check if the alternative is inside a 'file' directory + # path_to_file = os.path.join(env_dir, 'file', alternative) + + # Check if the alternative is inside a 'bin' directory + path_to_file = os.path.join(env_dir, "bin", alternative) + + if os.path.isfile(path_to_file): + if verbose: + print(f"[Found {filename}: {path_to_file}]") + yielded = True + yield path_to_file + + # Check the path list. + for directory in searchpath: + for alternative in file_names: + path_to_file = os.path.join(directory, alternative) + if os.path.isfile(path_to_file): + yielded = True + yield path_to_file + + # If we're on a POSIX system, then try using the 'which' command + # to find the file. + if os.name == "posix": + for alternative in file_names: + try: + p = subprocess.Popen( + ["which", alternative], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate() + path = _decode_stdoutdata(stdout).strip() + if path.endswith(alternative) and os.path.exists(path): + if verbose: + print(f"[Found {filename}: {path}]") + yielded = True + yield path + except (KeyboardInterrupt, SystemExit, OSError): + raise + finally: + pass + + if not yielded: + msg = ( + "NLTK was unable to find the %s file!" + "\nUse software specific " + "configuration parameters" % filename + ) + if env_vars: + msg += " or set the %s environment variable" % env_vars[0] + msg += "." + if searchpath: + msg += "\n\n Searched in:" + msg += "".join("\n - %s" % d for d in searchpath) + if url: + msg += f"\n\n For more information on {filename}, see:\n <{url}>" + div = "=" * 75 + raise LookupError(f"\n\n{div}\n{msg}\n{div}") + + +def find_file( + filename, env_vars=(), searchpath=(), file_names=None, url=None, verbose=False +): + return next( + find_file_iter(filename, env_vars, searchpath, file_names, url, verbose) + ) + + +def find_dir( + filename, env_vars=(), searchpath=(), file_names=None, url=None, verbose=False +): + return next( + find_file_iter( + filename, env_vars, searchpath, file_names, url, verbose, finding_dir=True + ) + ) + + +def find_binary_iter( + name, + path_to_bin=None, + env_vars=(), + searchpath=(), + binary_names=None, + url=None, + verbose=False, +): + """ + Search for a file to be used by nltk. + + :param name: The name or path of the file. + :param path_to_bin: The user-supplied binary location (deprecated) + :param env_vars: A list of environment variable names to check. + :param file_names: A list of alternative file names to check. + :param searchpath: List of directories to search. + :param url: URL presented to user for download help. + :param verbose: Whether or not to print path when a file is found. + """ + # Searching by a *bare* tool name (no explicit ``path_to_bin`` and no + # directory component in ``name``) is the insecure case: ``find_file_iter`` + # probes the current working directory for ``/`` and the bare + # name before the configured ``env_vars`` / ``searchpath``, so a planted + # ``.//...`` could be returned and -- because it contains a separator + # -- run relative to the CWD rather than looked up on PATH: arbitrary code + # execution (CWE-426 / CWE-427). Only in that case do we refuse CWD-relative + # matches and accept solely a trusted absolute location (env var / searchpath + # / ``which``). An explicit path supplied via ``path_to_bin`` or via ``name`` + # itself (e.g. ``tools/prover9``) is the caller's own choice and is honored + # as before. ``not path_to_bin`` (rather than ``is None``) so an empty-string + # path_to_bin -- which ``path_to_bin or name`` already falls back to ``name`` + # for -- cannot bypass the check. + searching_bare_name = not path_to_bin and os.path.dirname(name) == "" + safe_match = False + for path in find_file_iter( + path_to_bin or name, env_vars, searchpath, binary_names, url, verbose + ): + if searching_bare_name and not os.path.isabs(path): + continue + safe_match = True + yield path + if searching_bare_name and not safe_match: + # ``find_file_iter`` itself raises ``LookupError`` when nothing matches, + # so reaching here means it found only untrusted CWD-relative + # executables, which were rejected above. + raise LookupError( + f"NLTK found {name!r} only in the current working directory, which " + "is not a trusted location for executables. Install it on PATH or in " + "a configured location, or pass an explicit path_to_bin." + ) + + +def find_binary( + name, + path_to_bin=None, + env_vars=(), + searchpath=(), + binary_names=None, + url=None, + verbose=False, +): + return next( + find_binary_iter( + name, path_to_bin, env_vars, searchpath, binary_names, url, verbose + ) + ) + + +def find_jar_iter( + name_pattern, + path_to_jar=None, + env_vars=(), + searchpath=(), + url=None, + verbose=False, + is_regex=False, +): + """ + Search for a jar that is used by nltk. + + :param name_pattern: The name of the jar file + :param path_to_jar: The user-supplied jar location, or None. + :param env_vars: A list of environment variable names to check + in addition to the CLASSPATH variable which is + checked by default. + :param searchpath: List of directories to search. + :param is_regex: Whether name is a regular expression. + """ + + assert isinstance(name_pattern, str) + assert not isinstance(searchpath, str) + if isinstance(env_vars, str): + env_vars = env_vars.split() + yielded = False + + # Make sure we check the CLASSPATH first + env_vars = ["CLASSPATH"] + list(env_vars) + + # If an explicit location was given, then check it, and yield it if + # it's present; otherwise, complain. + if path_to_jar is not None: + if os.path.isfile(path_to_jar): + yielded = True + yield path_to_jar + else: + raise LookupError( + f"Could not find {name_pattern} jar file at {path_to_jar}" + ) + + # Check environment variables + for env_var in env_vars: + if env_var in os.environ: + if env_var == "CLASSPATH": + classpath = os.environ["CLASSPATH"] + for cp in classpath.split(os.path.pathsep): + cp = os.path.expanduser(cp) + if os.path.isfile(cp): + filename = os.path.basename(cp) + if ( + is_regex + and re.match(name_pattern, filename) + or (not is_regex and filename == name_pattern) + ): + if verbose: + print(f"[Found {name_pattern}: {cp}]") + yielded = True + yield cp + # The case where user put directory containing the jar file in the classpath + if os.path.isdir(cp): + if not is_regex: + if os.path.isfile(os.path.join(cp, name_pattern)): + if verbose: + print(f"[Found {name_pattern}: {cp}]") + yielded = True + yield os.path.join(cp, name_pattern) + else: + # Look for file using regular expression + for file_name in os.listdir(cp): + if re.match(name_pattern, file_name): + if verbose: + print( + "[Found %s: %s]" + % ( + name_pattern, + os.path.join(cp, file_name), + ) + ) + yielded = True + yield os.path.join(cp, file_name) + + else: + jar_env = os.path.expanduser(os.environ[env_var]) + jar_iter = ( + ( + os.path.join(jar_env, path_to_jar) + for path_to_jar in os.listdir(jar_env) + ) + if os.path.isdir(jar_env) + else (jar_env,) + ) + for path_to_jar in jar_iter: + if os.path.isfile(path_to_jar): + filename = os.path.basename(path_to_jar) + if ( + is_regex + and re.match(name_pattern, filename) + or (not is_regex and filename == name_pattern) + ): + if verbose: + print(f"[Found {name_pattern}: {path_to_jar}]") + yielded = True + yield path_to_jar + + # Check the path list. + for directory in searchpath: + if is_regex: + for filename in os.listdir(directory): + path_to_jar = os.path.join(directory, filename) + if os.path.isfile(path_to_jar): + if re.match(name_pattern, filename): + if verbose: + print(f"[Found {filename}: {path_to_jar}]") + yielded = True + yield path_to_jar + else: + path_to_jar = os.path.join(directory, name_pattern) + if os.path.isfile(path_to_jar): + if verbose: + print(f"[Found {name_pattern}: {path_to_jar}]") + yielded = True + yield path_to_jar + + if not yielded: + # If nothing was found, raise an error + msg = "NLTK was unable to find %s!" % name_pattern + if env_vars: + msg += " Set the %s environment variable" % env_vars[0] + msg = textwrap.fill(msg + ".", initial_indent=" ", subsequent_indent=" ") + if searchpath: + msg += "\n\n Searched in:" + msg += "".join("\n - %s" % d for d in searchpath) + if url: + msg += "\n\n For more information, on {}, see:\n <{}>".format( + name_pattern, + url, + ) + div = "=" * 75 + raise LookupError(f"\n\n{div}\n{msg}\n{div}") + + +def find_jar( + name_pattern, + path_to_jar=None, + env_vars=(), + searchpath=(), + url=None, + verbose=False, + is_regex=False, +): + return next( + find_jar_iter( + name_pattern, path_to_jar, env_vars, searchpath, url, verbose, is_regex + ) + ) + + +def find_jars_within_path(path_to_jars): + return [ + os.path.join(root, filename) + for root, dirnames, filenames in os.walk(path_to_jars) + for filename in fnmatch.filter(filenames, "*.jar") + ] + + +def _decode_stdoutdata(stdoutdata): + """Convert data read from stdout/stderr to unicode""" + if not isinstance(stdoutdata, bytes): + return stdoutdata + + encoding = getattr(sys.__stdout__, "encoding", locale.getpreferredencoding()) + if encoding is None: + return stdoutdata.decode() + return stdoutdata.decode(encoding) + + +########################################################################## +# Import Stdlib Module +########################################################################## + + +def import_from_stdlib(module): + """ + When python is run from within the nltk/ directory tree, the + current directory is included at the beginning of the search path. + Unfortunately, that means that modules within nltk can sometimes + shadow standard library modules. As an example, the stdlib + 'inspect' module will attempt to import the stdlib 'tokenize' + module, but will instead end up importing NLTK's 'tokenize' module + instead (causing the import to fail). + """ + old_path = sys.path + sys.path = [d for d in sys.path if d not in ("", ".")] + m = __import__(module) + sys.path = old_path + return m + + +########################################################################## +# Wrapper for ElementTree Elements +########################################################################## + + +class ElementWrapper: + """ + A wrapper around ElementTree Element objects whose main purpose is + to provide nicer __repr__ and __str__ methods. In addition, any + of the wrapped Element's methods that return other Element objects + are overridden to wrap those values before returning them. + + This makes Elements more convenient to work with in + interactive sessions and doctests, at the expense of some + efficiency. + """ + + # Prevent double-wrapping: + def __new__(cls, etree): + """ + Create and return a wrapper around a given Element object. + If ``etree`` is an ``ElementWrapper``, then ``etree`` is + returned as-is. + """ + if isinstance(etree, ElementWrapper): + return etree + else: + return object.__new__(ElementWrapper) + + def __init__(self, etree): + r""" + Initialize a new Element wrapper for ``etree``. + + If ``etree`` is a string, then it will be converted to an + Element object using ``ElementTree.fromstring()`` first: + + >>> ElementWrapper("") + \n"> + + """ + if isinstance(etree, str): + etree = ElementTree.fromstring(etree) + self.__dict__["_etree"] = etree + + def unwrap(self): + """ + Return the Element object wrapped by this wrapper. + """ + return self._etree + + ##//////////////////////////////////////////////////////////// + # { String Representation + ##//////////////////////////////////////////////////////////// + + def __repr__(self): + s = ElementTree.tostring(self._etree, encoding="utf8").decode("utf8") + if len(s) > 60: + e = s.rfind("<") + if (len(s) - e) > 30: + e = -20 + s = f"{s[:30]}...{s[e:]}" + return "" % s + + def __str__(self): + """ + :return: the result of applying ``ElementTree.tostring()`` to + the wrapped Element object. + """ + return ( + ElementTree.tostring(self._etree, encoding="utf8").decode("utf8").rstrip() + ) + + ##//////////////////////////////////////////////////////////// + # { Element interface Delegation (pass-through) + ##//////////////////////////////////////////////////////////// + + def __getattr__(self, attrib): + return getattr(self._etree, attrib) + + def __setattr__(self, attr, value): + return setattr(self._etree, attr, value) + + def __delattr__(self, attr): + return delattr(self._etree, attr) + + def __setitem__(self, index, element): + self._etree[index] = element + + def __delitem__(self, index): + del self._etree[index] + + def __setslice__(self, start, stop, elements): + self._etree[start:stop] = elements + + def __delslice__(self, start, stop): + del self._etree[start:stop] + + def __len__(self): + return len(self._etree) + + ##//////////////////////////////////////////////////////////// + # { Element interface Delegation (wrap result) + ##//////////////////////////////////////////////////////////// + + def __getitem__(self, index): + return ElementWrapper(self._etree[index]) + + def __getslice__(self, start, stop): + return [ElementWrapper(elt) for elt in self._etree[start:stop]] + + def getchildren(self): + return [ElementWrapper(elt) for elt in self._etree] + + def getiterator(self, tag=None): + return (ElementWrapper(elt) for elt in self._etree.getiterator(tag)) + + def makeelement(self, tag, attrib): + return ElementWrapper(self._etree.makeelement(tag, attrib)) + + def find(self, path): + elt = self._etree.find(path) + if elt is None: + return elt + else: + return ElementWrapper(elt) + + def findall(self, path): + return [ElementWrapper(elt) for elt in self._etree.findall(path)] + + +###################################################################### +# Helper for Handling Slicing +###################################################################### + + +def slice_bounds(sequence, slice_obj, allow_step=False): + """ + Given a slice, return the corresponding (start, stop) bounds, + taking into account None indices and negative indices. The + following guarantees are made for the returned start and stop values: + + - 0 <= start <= len(sequence) + - 0 <= stop <= len(sequence) + - start <= stop + + :raise ValueError: If ``slice_obj.step`` is not None. + :param allow_step: If true, then the slice object may have a + non-None step. If it does, then return a tuple + (start, stop, step). + """ + start, stop = (slice_obj.start, slice_obj.stop) + + # If allow_step is true, then include the step in our return + # value tuple. + if allow_step: + step = slice_obj.step + if step is None: + step = 1 + # Use a recursive call without allow_step to find the slice + # bounds. If step is negative, then the roles of start and + # stop (in terms of default values, etc), are swapped. + if step < 0: + start, stop = slice_bounds(sequence, slice(stop, start)) + else: + start, stop = slice_bounds(sequence, slice(start, stop)) + return start, stop, step + + # Otherwise, make sure that no non-default step value is used. + elif slice_obj.step not in (None, 1): + raise ValueError( + "slices with steps are not supported by %s" % sequence.__class__.__name__ + ) + + # Supply default offsets. + if start is None: + start = 0 + if stop is None: + stop = len(sequence) + + # Handle negative indices. + if start < 0: + start = max(0, len(sequence) + start) + if stop < 0: + stop = max(0, len(sequence) + stop) + + # Make sure stop doesn't go past the end of the list. Note that + # we avoid calculating len(sequence) if possible, because for lazy + # sequences, calculating the length of a sequence can be expensive. + if stop > 0: + try: + sequence[stop - 1] + except IndexError: + stop = len(sequence) + + # Make sure start isn't past stop. + start = min(start, stop) + + # That's all folks! + return start, stop + + +###################################################################### +# Permission Checking +###################################################################### + + +def is_writable(path): + # Ensure that it exists. + if not os.path.exists(path): + return False + + # If we're on a posix system, check its permissions. + if hasattr(os, "getuid"): + statdata = os.stat(path) + perm = stat.S_IMODE(statdata.st_mode) + # is it world-writable? + if perm & 0o002: + return True + # do we own it? + elif statdata.st_uid == os.getuid() and (perm & 0o200): + return True + # are we in a group that can write to it? + elif (statdata.st_gid in [os.getgid()] + os.getgroups()) and (perm & 0o020): + return True + # otherwise, we can't write to it. + else: + return False + + # Otherwise, we'll assume it's writable. + # [xx] should we do other checks on other platforms? + return True + + +###################################################################### +# NLTK Error reporting +###################################################################### + + +def raise_unorderable_types(ordering, a, b): + raise TypeError( + "unorderable types: %s() %s %s()" + % (type(a).__name__, ordering, type(b).__name__) + ) diff --git a/nltk/jsontags.py b/nltk/jsontags.py new file mode 100644 index 0000000..44716e3 --- /dev/null +++ b/nltk/jsontags.py @@ -0,0 +1,76 @@ +# Natural Language Toolkit: JSON Encoder/Decoder Helpers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Xu +# +# URL: +# For license information, see LICENSE.TXT + +""" +Register JSON tags, so the nltk data loader knows what module and class to look for. + +NLTK uses simple '!' tags to mark the types of objects, but the fully-qualified +"tag:nltk.org,2011:" prefix is also accepted in case anyone ends up +using it. +""" + +import json + +json_tags = {} + +TAG_PREFIX = "!" + + +def register_tag(cls): + """ + Decorates a class to register it's json tag. + """ + json_tags[TAG_PREFIX + getattr(cls, "json_tag")] = cls + return cls + + +class JSONTaggedEncoder(json.JSONEncoder): + def default(self, obj): + obj_tag = getattr(obj, "json_tag", None) + if obj_tag is None: + return super().default(obj) + obj_tag = TAG_PREFIX + obj_tag + obj = obj.encode_json_obj() + return {obj_tag: obj} + + +class JSONTaggedDecoder(json.JSONDecoder): + #: Maximum nesting depth for decoded JSON objects. + #: Prevents denial of service from deeply nested payloads. + MAX_DECODE_DEPTH = 200 + + def decode(self, s): + try: + return self.decode_obj(super().decode(s)) + except RecursionError: + raise ValueError("JSON nesting too deep to decode safely") + + @classmethod + def decode_obj(cls, obj, _depth=0): + if _depth > cls.MAX_DECODE_DEPTH: + raise ValueError( + f"JSON nesting depth exceeds maximum allowed ({cls.MAX_DECODE_DEPTH})" + ) + # Decode nested objects first. + if isinstance(obj, dict): + obj = {key: cls.decode_obj(val, _depth + 1) for (key, val) in obj.items()} + elif isinstance(obj, list): + obj = list(cls.decode_obj(val, _depth + 1) for val in obj) + # Check if we have a tagged object. + if not isinstance(obj, dict) or len(obj) != 1: + return obj + obj_tag = next(iter(obj.keys())) + if not obj_tag.startswith("!"): + return obj + if obj_tag not in json_tags: + raise ValueError("Unknown tag", obj_tag) + obj_cls = json_tags[obj_tag] + return obj_cls.decode_json_obj(obj[obj_tag]) + + +__all__ = ["register_tag", "json_tags", "JSONTaggedEncoder", "JSONTaggedDecoder"] diff --git a/nltk/langnames.py b/nltk/langnames.py new file mode 100644 index 0000000..b76f06a --- /dev/null +++ b/nltk/langnames.py @@ -0,0 +1,828 @@ +# Natural Language Toolkit: Language Codes +# +# Copyright (C) 2022-2025 NLTK Project +# Author: Eric Kafe +# URL: +# For license information, see LICENSE.TXT +# +# iso639-3 language codes (C) https://iso639-3.sil.org/ + +""" +Translate between language names and language codes. + +The iso639-3 language codes were downloaded from the registration authority at +https://iso639-3.sil.org/ + +The iso639-3 codeset is evolving, so retired language codes are kept in the +"iso639retired" dictionary, which is used as fallback by the wrapper functions +"langname" and "langcode", in order to support the lookup of retired codes. + +The "langcode" function returns the current iso639-3 code if there is one, +and falls back to the retired code otherwise. As specified by BCP-47, +it returns the shortest (2-letter) code by default, but 3-letter codes +are also available: + + >>> import nltk.langnames as lgn + >>> lgn.langname('fri') #'fri' is a retired code + 'Western Frisian' + + The current code is different from the retired one: + >>> lgn.langcode('Western Frisian') + 'fy' + + >>> lgn.langcode('Western Frisian', typ = 3) + 'fry' + +""" + +import re +from warnings import warn + +from nltk.corpus import bcp47 + +codepattern = re.compile("[a-z][a-z][a-z]?") + + +def langname(tag, typ="full", strict=False): + """ + Convert a composite BCP-47 tag to a language name. + + Returns None if the tag is not found, unless strict=True, + in which case a LookupError is raised. + + >>> from nltk.langnames import langname + >>> langname('ca-Latn-ES-valencia') + 'Catalan: Latin: Spain: Valencian' + + >>> langname('ca-Latn-ES-valencia', typ="short") + 'Catalan' + + >>> print(langname('zzz')) + None + + >>> langname('zzz', strict=True) + Traceback (most recent call last): + ... + LookupError: Could not find language name for tag 'zzz' + """ + if tag is None: + if strict: + raise LookupError("Could not find language name for tag None") + return None + tags = tag.split("-") + code = tags[0].lower() + if codepattern.fullmatch(code): + if code in iso639retired: # retired codes + return iso639retired[code] + elif code in iso639short: # 3-letter codes + code2 = iso639short[code] # convert to 2-letter code + warn(f"Shortening {code!r} to {code2!r}", stacklevel=2) + tag = "-".join([code2] + tags[1:]) + name = bcp47.name(tag) # parse according to BCP-47 + if name is not None: + if typ == "full": + return name # include all subtags + else: + return name.split(":")[0] # only the language subtag + failed = f"Could not find language name for tag {tag!r}" + if strict: + raise LookupError(failed) + warn(failed, stacklevel=2) + + +def langcode(name, typ=2, strict=False): + """ + Convert language name to iso639-3 language code. Returns the short 2-letter + code by default, if one is available, and the 3-letter code otherwise. + + Returns None if the name is not found, unless strict=True, + in which case a LookupError is raised. + + >>> from nltk.langnames import langcode + >>> langcode('Modern Greek (1453-)') + 'el' + + Specify 'typ=3' to get the 3-letter code: + + >>> langcode('Modern Greek (1453-)', typ=3) + 'ell' + + >>> print(langcode('NotALanguage')) + None + + >>> langcode('NotALanguage', strict=True) + Traceback (most recent call last): + ... + LookupError: Could not find language code for name 'NotALanguage' + """ + if name in bcp47.langcode: + code = bcp47.langcode[name] + if typ == 3 and code in iso639long: + code = iso639long[code] # convert to 3-letter code + return code + elif name in iso639code_retired: + return iso639code_retired[name] + elif strict: + raise LookupError(f"Could not find language code for name {name!r}") + else: + warn(f"Could not find language in {name!r}", stacklevel=2) + + +# ======================================================================= +# Translate betwwen Wikidata Q-codes and BCP-47 codes or names +# ....................................................................... + + +def tag2q(tag, strict=False): + """ + Convert BCP-47 tag to Wikidata Q-code. + + Returns the Wikidata Q-code for the given BCP-47 tag, or None if the tag + is not found. If strict=True, raises a LookupError instead of returning None. + + >>> tag2q('nds-u-sd-demv') + 'Q4289225' + >>> print(tag2q('unknown-tag')) + None + + >>> tag2q('unknown-tag', strict=True) + Traceback (most recent call last): + ... + LookupError: Could not find Wikidata Q-code for BCP-47 tag 'unknown-tag' + """ + if not hasattr(bcp47, "wiki_q") or bcp47.wiki_q is None: + bcp47.load_wiki_q() # Wikidata conversion table needs to be loaded explicitly + result = bcp47.wiki_q.get(tag) + if result is None and strict: + raise LookupError(f"Could not find Wikidata Q-code for BCP-47 tag {tag!r}") + return result + + +def inverse_dict(dic): + """ + Return the inverse mapping of a dictionary if it is bijective. + + If the input dictionary is bijective (i.e., all values are unique), returns a new dictionary + mapping values to keys. Otherwise, returns None and emits a warning. + """ + if len(dic.keys()) == len(set(dic.values())): + return {val: key for (key, val) in dic.items()} + else: + warn("This dictionary has no bijective inverse mapping.") + + +def q2tag(qcode, strict=False): + """ + Convert Wikidata Q-code to BCP-47 tag. + + Returns the BCP-47 tag for the given Wikidata Q-code, or None if the Q-code + is not found. If strict=True, raises a LookupError instead of returning None. + + >>> q2tag('Q4289225') + 'nds-u-sd-demv' + >>> print(q2tag('Q0000000')) + None + + >>> q2tag('Q0000000', strict=True) + Traceback (most recent call last): + ... + LookupError: Could not find BCP-47 tag for Wikidata Q-code 'Q0000000' + """ + if not hasattr(bcp47, "wiki_q") or bcp47.wiki_q is None: + bcp47.load_wiki_q() # Wikidata conversion table needs to be loaded explicitly + if not hasattr(bcp47, "wiki_bcp47") or bcp47.wiki_bcp47 is None: + inverse = inverse_dict(bcp47.wiki_q) + if inverse is None: + raise ValueError( + "The Wikidata mapping (wiki_q) is not bijective. Cannot safely build inverse mapping." + ) + bcp47.wiki_bcp47 = inverse + result = bcp47.wiki_bcp47.get(qcode) + if result is None and strict: + raise LookupError(f"Could not find BCP-47 tag for Wikidata Q-code {qcode!r}") + return result + + +def q2name(qcode, typ="full", strict=False): + """ + Convert Wikidata Q-code to BCP-47 (full or short) language name. + + Returns None if the Q-code is not found, unless strict=True, + in which case a LookupError is raised. + + >>> q2name('Q4289225') + 'Low German: Mecklenburg-Vorpommern' + + >>> q2name('Q4289225', "short") + 'Low German' + + >>> print(q2name('Q0000000')) + None + + >>> q2name('Q0000000', strict=True) + Traceback (most recent call last): + ... + LookupError: Could not find BCP-47 tag for Wikidata Q-code 'Q0000000' + """ + tag = q2tag(qcode, strict=strict) + if tag is None: + return None + return langname(tag, typ, strict=strict) + + +def lang2q(name, strict=False): + """ + Convert simple language name to Wikidata Q-code. + + Returns None if the name is not found, unless strict=True, + in which case a LookupError is raised. + + >>> lang2q('Low German') + 'Q25433' + + >>> print(lang2q('NonexistentLanguage')) + None + + >>> lang2q('NonexistentLanguage', strict=True) + Traceback (most recent call last): + ... + LookupError: Could not find language code for name 'NonexistentLanguage' + """ + code = langcode(name, strict=strict) + if code is None: + return None + return tag2q(code, strict=strict) + + +# ====================================================================== +# Data dictionaries +# ...................................................................... + + +iso639short = { + "aar": "aa", + "abk": "ab", + "afr": "af", + "aka": "ak", + "amh": "am", + "ara": "ar", + "arg": "an", + "asm": "as", + "ava": "av", + "ave": "ae", + "aym": "ay", + "aze": "az", + "bak": "ba", + "bam": "bm", + "bel": "be", + "ben": "bn", + "bis": "bi", + "bod": "bo", + "bos": "bs", + "bre": "br", + "bul": "bg", + "cat": "ca", + "ces": "cs", + "cha": "ch", + "che": "ce", + "chu": "cu", + "chv": "cv", + "cor": "kw", + "cos": "co", + "cre": "cr", + "cym": "cy", + "dan": "da", + "deu": "de", + "div": "dv", + "dzo": "dz", + "ell": "el", + "eng": "en", + "epo": "eo", + "est": "et", + "eus": "eu", + "ewe": "ee", + "fao": "fo", + "fas": "fa", + "fij": "fj", + "fin": "fi", + "fra": "fr", + "fry": "fy", + "ful": "ff", + "gla": "gd", + "gle": "ga", + "glg": "gl", + "glv": "gv", + "grn": "gn", + "guj": "gu", + "hat": "ht", + "hau": "ha", + "hbs": "sh", + "heb": "he", + "her": "hz", + "hin": "hi", + "hmo": "ho", + "hrv": "hr", + "hun": "hu", + "hye": "hy", + "ibo": "ig", + "ido": "io", + "iii": "ii", + "iku": "iu", + "ile": "ie", + "ina": "ia", + "ind": "id", + "ipk": "ik", + "isl": "is", + "ita": "it", + "jav": "jv", + "jpn": "ja", + "kal": "kl", + "kan": "kn", + "kas": "ks", + "kat": "ka", + "kau": "kr", + "kaz": "kk", + "khm": "km", + "kik": "ki", + "kin": "rw", + "kir": "ky", + "kom": "kv", + "kon": "kg", + "kor": "ko", + "kua": "kj", + "kur": "ku", + "lao": "lo", + "lat": "la", + "lav": "lv", + "lim": "li", + "lin": "ln", + "lit": "lt", + "ltz": "lb", + "lub": "lu", + "lug": "lg", + "mah": "mh", + "mal": "ml", + "mar": "mr", + "mkd": "mk", + "mlg": "mg", + "mlt": "mt", + "mon": "mn", + "mri": "mi", + "msa": "ms", + "mya": "my", + "nau": "na", + "nav": "nv", + "nbl": "nr", + "nde": "nd", + "ndo": "ng", + "nep": "ne", + "nld": "nl", + "nno": "nn", + "nob": "nb", + "nor": "no", + "nya": "ny", + "oci": "oc", + "oji": "oj", + "ori": "or", + "orm": "om", + "oss": "os", + "pan": "pa", + "pli": "pi", + "pol": "pl", + "por": "pt", + "pus": "ps", + "que": "qu", + "roh": "rm", + "ron": "ro", + "run": "rn", + "rus": "ru", + "sag": "sg", + "san": "sa", + "sin": "si", + "slk": "sk", + "slv": "sl", + "sme": "se", + "smo": "sm", + "sna": "sn", + "snd": "sd", + "som": "so", + "sot": "st", + "spa": "es", + "sqi": "sq", + "srd": "sc", + "srp": "sr", + "ssw": "ss", + "sun": "su", + "swa": "sw", + "swe": "sv", + "tah": "ty", + "tam": "ta", + "tat": "tt", + "tel": "te", + "tgk": "tg", + "tgl": "tl", + "tha": "th", + "tir": "ti", + "ton": "to", + "tsn": "tn", + "tso": "ts", + "tuk": "tk", + "tur": "tr", + "twi": "tw", + "uig": "ug", + "ukr": "uk", + "urd": "ur", + "uzb": "uz", + "ven": "ve", + "vie": "vi", + "vol": "vo", + "wln": "wa", + "wol": "wo", + "xho": "xh", + "yid": "yi", + "yor": "yo", + "zha": "za", + "zho": "zh", + "zul": "zu", +} + + +iso639retired = { + "fri": "Western Frisian", + "auv": "Auvergnat", + "gsc": "Gascon", + "lms": "Limousin", + "lnc": "Languedocien", + "prv": "Provençal", + "amd": "Amapá Creole", + "bgh": "Bogan", + "bnh": "Banawá", + "bvs": "Belgian Sign Language", + "ccy": "Southern Zhuang", + "cit": "Chittagonian", + "flm": "Falam Chin", + "jap": "Jaruára", + "kob": "Kohoroxitari", + "mob": "Moinba", + "mzf": "Aiku", + "nhj": "Tlalitzlipa Nahuatl", + "nhs": "Southeastern Puebla Nahuatl", + "occ": "Occidental", + "tmx": "Tomyang", + "tot": "Patla-Chicontla Totonac", + "xmi": "Miarrã", + "yib": "Yinglish", + "ztc": "Lachirioag Zapotec", + "atf": "Atuence", + "bqe": "Navarro-Labourdin Basque", + "bsz": "Souletin Basque", + "aex": "Amerax", + "ahe": "Ahe", + "aiz": "Aari", + "akn": "Amikoana", + "arf": "Arafundi", + "azr": "Adzera", + "bcx": "Pamona", + "bii": "Bisu", + "bke": "Bengkulu", + "blu": "Hmong Njua", + "boc": "Bakung Kenyah", + "bsd": "Sarawak Bisaya", + "bwv": "Bahau River Kenyah", + "bxt": "Buxinhua", + "byu": "Buyang", + "ccx": "Northern Zhuang", + "cru": "Carútana", + "dat": "Darang Deng", + "dyk": "Land Dayak", + "eni": "Enim", + "fiz": "Izere", + "gen": "Geman Deng", + "ggh": "Garreh-Ajuran", + "itu": "Itutang", + "kds": "Lahu Shi", + "knh": "Kayan River Kenyah", + "krg": "North Korowai", + "krq": "Krui", + "kxg": "Katingan", + "lmt": "Lematang", + "lnt": "Lintang", + "lod": "Berawan", + "mbg": "Northern Nambikuára", + "mdo": "Southwest Gbaya", + "mhv": "Arakanese", + "miv": "Mimi", + "mqd": "Madang", + "nky": "Khiamniungan Naga", + "nxj": "Nyadu", + "ogn": "Ogan", + "ork": "Orokaiva", + "paj": "Ipeka-Tapuia", + "pec": "Southern Pesisir", + "pen": "Penesak", + "plm": "Palembang", + "poj": "Lower Pokomo", + "pun": "Pubian", + "rae": "Ranau", + "rjb": "Rajbanshi", + "rws": "Rawas", + "sdd": "Semendo", + "sdi": "Sindang Kelingi", + "skl": "Selako", + "slb": "Kahumamahon Saluan", + "srj": "Serawai", + "suf": "Tarpia", + "suh": "Suba", + "suu": "Sungkai", + "szk": "Sizaki", + "tle": "Southern Marakwet", + "tnj": "Tanjong", + "ttx": "Tutong 1", + "ubm": "Upper Baram Kenyah", + "vky": "Kayu Agung", + "vmo": "Muko-Muko", + "wre": "Ware", + "xah": "Kahayan", + "xkm": "Mahakam Kenyah", + "xuf": "Kunfal", + "yio": "Dayao Yi", + "ymj": "Muji Yi", + "ypl": "Pula Yi", + "ypw": "Puwa Yi", + "ywm": "Wumeng Yi", + "yym": "Yuanjiang-Mojiang Yi", + "mly": "Malay (individual language)", + "muw": "Mundari", + "xst": "Silt'e", + "ope": "Old Persian", + "scc": "Serbian", + "scr": "Croatian", + "xsk": "Sakan", + "mol": "Moldavian", + "aay": "Aariya", + "acc": "Cubulco Achí", + "cbm": "Yepocapa Southwestern Cakchiquel", + "chs": "Chumash", + "ckc": "Northern Cakchiquel", + "ckd": "South Central Cakchiquel", + "cke": "Eastern Cakchiquel", + "ckf": "Southern Cakchiquel", + "cki": "Santa María De Jesús Cakchiquel", + "ckj": "Santo Domingo Xenacoj Cakchiquel", + "ckk": "Acatenango Southwestern Cakchiquel", + "ckw": "Western Cakchiquel", + "cnm": "Ixtatán Chuj", + "cti": "Tila Chol", + "cun": "Cunén Quiché", + "eml": "Emiliano-Romagnolo", + "eur": "Europanto", + "gmo": "Gamo-Gofa-Dawro", + "hsf": "Southeastern Huastec", + "hva": "San Luís Potosí Huastec", + "ixi": "Nebaj Ixil", + "ixj": "Chajul Ixil", + "jai": "Western Jacalteco", + "mms": "Southern Mam", + "mpf": "Tajumulco Mam", + "mtz": "Tacanec", + "mvc": "Central Mam", + "mvj": "Todos Santos Cuchumatán Mam", + "poa": "Eastern Pokomam", + "pob": "Western Pokomchí", + "pou": "Southern Pokomam", + "ppv": "Papavô", + "quj": "Joyabaj Quiché", + "qut": "West Central Quiché", + "quu": "Eastern Quiché", + "qxi": "San Andrés Quiché", + "sic": "Malinguat", + "stc": "Santa Cruz", + "tlz": "Toala'", + "tzb": "Bachajón Tzeltal", + "tzc": "Chamula Tzotzil", + "tze": "Chenalhó Tzotzil", + "tzs": "San Andrés Larrainzar Tzotzil", + "tzt": "Western Tzutujil", + "tzu": "Huixtán Tzotzil", + "tzz": "Zinacantán Tzotzil", + "vlr": "Vatrata", + "yus": "Chan Santa Cruz Maya", + "nfg": "Nyeng", + "nfk": "Shakara", + "agp": "Paranan", + "bhk": "Albay Bicolano", + "bkb": "Finallig", + "btb": "Beti (Cameroon)", + "cjr": "Chorotega", + "cmk": "Chimakum", + "drh": "Darkhat", + "drw": "Darwazi", + "gav": "Gabutamon", + "mof": "Mohegan-Montauk-Narragansett", + "mst": "Cataelano Mandaya", + "myt": "Sangab Mandaya", + "rmr": "Caló", + "sgl": "Sanglechi-Ishkashimi", + "sul": "Surigaonon", + "sum": "Sumo-Mayangna", + "tnf": "Tangshewi", + "wgw": "Wagawaga", + "ayx": "Ayi (China)", + "bjq": "Southern Betsimisaraka Malagasy", + "dha": "Dhanwar (India)", + "dkl": "Kolum So Dogon", + "mja": "Mahei", + "nbf": "Naxi", + "noo": "Nootka", + "tie": "Tingal", + "tkk": "Takpa", + "baz": "Tunen", + "bjd": "Bandjigali", + "ccq": "Chaungtha", + "cka": "Khumi Awa Chin", + "dap": "Nisi (India)", + "dwl": "Walo Kumbe Dogon", + "elp": "Elpaputih", + "gbc": "Garawa", + "gio": "Gelao", + "hrr": "Horuru", + "ibi": "Ibilo", + "jar": "Jarawa (Nigeria)", + "kdv": "Kado", + "kgh": "Upper Tanudan Kalinga", + "kpp": "Paku Karen", + "kzh": "Kenuzi-Dongola", + "lcq": "Luhu", + "mgx": "Omati", + "nln": "Durango Nahuatl", + "pbz": "Palu", + "pgy": "Pongyong", + "sca": "Sansu", + "tlw": "South Wemale", + "unp": "Worora", + "wiw": "Wirangu", + "ybd": "Yangbye", + "yen": "Yendang", + "yma": "Yamphe", + "daf": "Dan", + "djl": "Djiwarli", + "ggr": "Aghu Tharnggalu", + "ilw": "Talur", + "izi": "Izi-Ezaa-Ikwo-Mgbo", + "meg": "Mea", + "mld": "Malakhel", + "mnt": "Maykulan", + "mwd": "Mudbura", + "myq": "Forest Maninka", + "nbx": "Ngura", + "nlr": "Ngarla", + "pcr": "Panang", + "ppr": "Piru", + "tgg": "Tangga", + "wit": "Wintu", + "xia": "Xiandao", + "yiy": "Yir Yoront", + "yos": "Yos", + "emo": "Emok", + "ggm": "Gugu Mini", + "leg": "Lengua", + "lmm": "Lamam", + "mhh": "Maskoy Pidgin", + "puz": "Purum Naga", + "sap": "Sanapaná", + "yuu": "Yugh", + "aam": "Aramanik", + "adp": "Adap", + "aue": "ǂKxʼauǁʼein", + "bmy": "Bemba (Democratic Republic of Congo)", + "bxx": "Borna (Democratic Republic of Congo)", + "byy": "Buya", + "dzd": "Daza", + "gfx": "Mangetti Dune ǃXung", + "gti": "Gbati-ri", + "ime": "Imeraguen", + "kbf": "Kakauhua", + "koj": "Sara Dunjo", + "kwq": "Kwak", + "kxe": "Kakihum", + "lii": "Lingkhim", + "mwj": "Maligo", + "nnx": "Ngong", + "oun": "ǃOǃung", + "pmu": "Mirpur Panjabi", + "sgo": "Songa", + "thx": "The", + "tsf": "Southwestern Tamang", + "uok": "Uokha", + "xsj": "Subi", + "yds": "Yiddish Sign Language", + "ymt": "Mator-Taygi-Karagas", + "ynh": "Yangho", + "bgm": "Baga Mboteni", + "btl": "Bhatola", + "cbe": "Chipiajes", + "cbh": "Cagua", + "coy": "Coyaima", + "cqu": "Chilean Quechua", + "cum": "Cumeral", + "duj": "Dhuwal", + "ggn": "Eastern Gurung", + "ggo": "Southern Gondi", + "guv": "Gey", + "iap": "Iapama", + "ill": "Iranun", + "kgc": "Kasseng", + "kox": "Coxima", + "ktr": "Kota Marudu Tinagas", + "kvs": "Kunggara", + "kzj": "Coastal Kadazan", + "kzt": "Tambunan Dusun", + "nad": "Nijadali", + "nts": "Natagaimas", + "ome": "Omejes", + "pmc": "Palumata", + "pod": "Ponares", + "ppa": "Pao", + "pry": "Pray 3", + "rna": "Runa", + "svr": "Savara", + "tdu": "Tempasuk Dusun", + "thc": "Tai Hang Tong", + "tid": "Tidong", + "tmp": "Tai Mène", + "tne": "Tinoc Kallahan", + "toe": "Tomedes", + "xba": "Kamba (Brazil)", + "xbx": "Kabixí", + "xip": "Xipináwa", + "xkh": "Karahawyana", + "yri": "Yarí", + "jeg": "Jeng", + "kgd": "Kataang", + "krm": "Krim", + "prb": "Lua'", + "puk": "Pu Ko", + "rie": "Rien", + "rsi": "Rennellese Sign Language", + "skk": "Sok", + "snh": "Shinabo", + "lsg": "Lyons Sign Language", + "mwx": "Mediak", + "mwy": "Mosiro", + "ncp": "Ndaktup", + "ais": "Nataoran Amis", + "asd": "Asas", + "dit": "Dirari", + "dud": "Hun-Saare", + "lba": "Lui", + "llo": "Khlor", + "myd": "Maramba", + "myi": "Mina (India)", + "nns": "Ningye", + "aoh": "Arma", + "ayy": "Tayabas Ayta", + "bbz": "Babalia Creole Arabic", + "bpb": "Barbacoas", + "cca": "Cauca", + "cdg": "Chamari", + "dgu": "Degaru", + "drr": "Dororo", + "ekc": "Eastern Karnic", + "gli": "Guliguli", + "kjf": "Khalaj", + "kxl": "Nepali Kurux", + "kxu": "Kui (India)", + "lmz": "Lumbee", + "nxu": "Narau", + "plp": "Palpa", + "sdm": "Semandang", + "tbb": "Tapeba", + "xrq": "Karranga", + "xtz": "Tasmanian", + "zir": "Ziriya", + "thw": "Thudam", + "bic": "Bikaru", + "bij": "Vaghat-Ya-Bijim-Legeri", + "blg": "Balau", + "gji": "Geji", + "mvm": "Muya", + "ngo": "Ngoni", + "pat": "Papitalai", + "vki": "Ija-Zuba", + "wra": "Warapu", + "ajt": "Judeo-Tunisian Arabic", + "cug": "Chungmboko", + "lak": "Laka (Nigeria)", + "lno": "Lango (South Sudan)", + "pii": "Pini", + "smd": "Sama", + "snb": "Sebuyau", + "uun": "Kulon-Pazeh", + "wrd": "Warduji", + "wya": "Wyandot", +} + + +iso639long = inverse_dict(iso639short) + +iso639code_retired = inverse_dict(iso639retired) diff --git a/nltk/lazyimport.py b/nltk/lazyimport.py new file mode 100644 index 0000000..e75b328 --- /dev/null +++ b/nltk/lazyimport.py @@ -0,0 +1,137 @@ +# This module is from mx/DateTime/LazyModule.py and is +# distributed under the terms of the eGenix.com Public License Agreement +# https://www.egenix.com/products/eGenix.com-Public-License-1.1.0.pdf + +"""Helper to enable simple lazy module import. + +'Lazy' means the actual import is deferred until an attribute is +requested from the module's namespace. This has the advantage of +allowing all imports to be done at the top of a script (in a +prominent and visible place) without having a great impact +on startup time. + +Copyright (c) 1999-2005, Marc-Andre Lemburg; mailto:mal@lemburg.com +See the documentation for further information on copyrights, +or contact the author. All Rights Reserved. +""" + +### Constants + +_debug = 0 + +### + + +class LazyModule: + """Lazy module class. + + Lazy modules are imported into the given namespaces whenever a + non-special attribute (there are some attributes like __doc__ + that class instances handle without calling __getattr__) is + requested. The module is then registered under the given name + in locals usually replacing the import wrapper instance. The + import itself is done using globals as global namespace. + + Example of creating a lazy load module: + + ISO = LazyModule('ISO',locals(),globals()) + + Later, requesting an attribute from ISO will load the module + automatically into the locals() namespace, overriding the + LazyModule instance: + + t = ISO.Week(1998,1,1) + + """ + + # Flag which indicates whether the LazyModule is initialized or not + __lazymodule_init = 0 + + # Name of the module to load + __lazymodule_name = "" + + # Flag which indicates whether the module was loaded or not + __lazymodule_loaded = 0 + + # Locals dictionary where to register the module + __lazymodule_locals = None + + # Globals dictionary to use for the module import + __lazymodule_globals = None + + def __init__(self, name, locals, globals=None): + """Create a LazyModule instance wrapping module name. + + The module will later on be registered in locals under the + given module name. + + globals is optional and defaults to locals. + + """ + self.__lazymodule_locals = locals + if globals is None: + globals = locals + self.__lazymodule_globals = globals + mainname = globals.get("__name__", "") + if mainname: + self.__name__ = mainname + "." + name + self.__lazymodule_name = name + else: + self.__name__ = self.__lazymodule_name = name + self.__lazymodule_init = 1 + + def __lazymodule_import(self): + """Import the module now.""" + # Load and register module + local_name = self.__lazymodule_name # e.g. "toolbox" + full_name = self.__name__ # e.g. "nltk.toolbox" + if self.__lazymodule_loaded: + return self.__lazymodule_locals[local_name] + if _debug: + print("LazyModule: Loading module %r" % full_name) + self.__lazymodule_locals[local_name] = module = __import__( + full_name, self.__lazymodule_locals, self.__lazymodule_globals, "*" + ) + + # Fill namespace with all symbols from original module to + # provide faster access. + self.__dict__.update(module.__dict__) + + # Set import flag + self.__dict__["__lazymodule_loaded"] = 1 + + if _debug: + print("LazyModule: Module %r loaded" % full_name) + return module + + def __getattr__(self, name): + """Import the module on demand and get the attribute.""" + if self.__lazymodule_loaded: + raise AttributeError(name) + if _debug: + print( + "LazyModule: " + "Module load triggered by attribute %r read access" % name + ) + module = self.__lazymodule_import() + return getattr(module, name) + + def __setattr__(self, name, value): + """Import the module on demand and set the attribute.""" + if not self.__lazymodule_init: + self.__dict__[name] = value + return + if self.__lazymodule_loaded: + self.__lazymodule_locals[self.__lazymodule_name] = value + self.__dict__[name] = value + return + if _debug: + print( + "LazyModule: " + "Module load triggered by attribute %r write access" % name + ) + module = self.__lazymodule_import() + setattr(module, name, value) + + def __repr__(self): + return "" % self.__name__ diff --git a/nltk/lm/__init__.py b/nltk/lm/__init__.py new file mode 100644 index 0000000..407f1d3 --- /dev/null +++ b/nltk/lm/__init__.py @@ -0,0 +1,235 @@ +# Natural Language Toolkit: Language Models +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Ilia Kurenkov +# URL: >> text = [['a', 'b', 'c'], ['a', 'c', 'd', 'c', 'e', 'f']] + +If we want to train a bigram model, we need to turn this text into bigrams. +Here's what the first sentence of our text would look like if we use a function +from NLTK for this. + + >>> from nltk.util import bigrams + >>> list(bigrams(text[0])) + [('a', 'b'), ('b', 'c')] + +Notice how "b" occurs both as the first and second member of different bigrams +but "a" and "c" don't? Wouldn't it be nice to somehow indicate how often sentences +start with "a" and end with "c"? +A standard way to deal with this is to add special "padding" symbols to the +sentence before splitting it into ngrams. +Fortunately, NLTK also has a function for that, let's see what it does to the +first sentence. + + >>> from nltk.util import pad_sequence + >>> list(pad_sequence(text[0], + ... pad_left=True, + ... left_pad_symbol="", + ... pad_right=True, + ... right_pad_symbol="", + ... n=2)) + ['', 'a', 'b', 'c', ''] + +Note the `n` argument, that tells the function we need padding for bigrams. +Now, passing all these parameters every time is tedious and in most cases they +can be safely assumed as defaults anyway. +Thus our module provides a convenience function that has all these arguments +already set while the other arguments remain the same as for `pad_sequence`. + + >>> from nltk.lm.preprocessing import pad_both_ends + >>> list(pad_both_ends(text[0], n=2)) + ['', 'a', 'b', 'c', ''] + +Combining the two parts discussed so far we get the following preparation steps +for one sentence. + + >>> list(bigrams(pad_both_ends(text[0], n=2))) + [('', 'a'), ('a', 'b'), ('b', 'c'), ('c', '')] + +To make our model more robust we could also train it on unigrams (single words) +as well as bigrams, its main source of information. +NLTK once again helpfully provides a function called `everygrams`. +While not the most efficient, it is conceptually simple. + + + >>> from nltk.util import everygrams + >>> padded_bigrams = list(pad_both_ends(text[0], n=2)) + >>> list(everygrams(padded_bigrams, max_len=2)) + [('',), ('', 'a'), ('a',), ('a', 'b'), ('b',), ('b', 'c'), ('c',), ('c', ''), ('',)] + +We are almost ready to start counting ngrams, just one more step left. +During training and evaluation our model will rely on a vocabulary that +defines which words are "known" to the model. +To create this vocabulary we need to pad our sentences (just like for counting +ngrams) and then combine the sentences into one flat stream of words. + + >>> from nltk.lm.preprocessing import flatten + >>> list(flatten(pad_both_ends(sent, n=2) for sent in text)) + ['', 'a', 'b', 'c', '', '', 'a', 'c', 'd', 'c', 'e', 'f', ''] + +In most cases we want to use the same text as the source for both vocabulary +and ngram counts. +Now that we understand what this means for our preprocessing, we can simply import +a function that does everything for us. + + >>> from nltk.lm.preprocessing import padded_everygram_pipeline + >>> train, vocab = padded_everygram_pipeline(2, text) + +So as to avoid re-creating the text in memory, both `train` and `vocab` are lazy +iterators. They are evaluated on demand at training time. + + +Training +======== +Having prepared our data we are ready to start training a model. +As a simple example, let us train a Maximum Likelihood Estimator (MLE). +We only need to specify the highest ngram order to instantiate it. + + >>> from nltk.lm import MLE + >>> lm = MLE(2) + +This automatically creates an empty vocabulary... + + >>> len(lm.vocab) + 0 + +... which gets filled as we fit the model. + + >>> lm.fit(train, vocab) + >>> print(lm.vocab) + + >>> len(lm.vocab) + 9 + +The vocabulary helps us handle words that have not occurred during training. + + >>> lm.vocab.lookup(text[0]) + ('a', 'b', 'c') + >>> lm.vocab.lookup(["aliens", "from", "Mars"]) + ('', '', '') + +Moreover, in some cases we want to ignore words that we did see during training +but that didn't occur frequently enough, to provide us useful information. +You can tell the vocabulary to ignore such words. +To find out how that works, check out the docs for the `Vocabulary` class. + + +Using a Trained Model +===================== +When it comes to ngram models the training boils down to counting up the ngrams +from the training corpus. + + >>> print(lm.counts) + + +This provides a convenient interface to access counts for unigrams... + + >>> lm.counts['a'] + 2 + +...and bigrams (in this case "a b") + + >>> lm.counts[['a']]['b'] + 1 + +And so on. However, the real purpose of training a language model is to have it +score how probable words are in certain contexts. +This being MLE, the model returns the item's relative frequency as its score. + + >>> lm.score("a") + 0.15384615384615385 + +Items that are not seen during training are mapped to the vocabulary's +"unknown label" token. This is "" by default. + + >>> lm.score("") == lm.score("aliens") + True + +Here's how you get the score for a word given some preceding context. +For example we want to know what is the chance that "b" is preceded by "a". + + >>> lm.score("b", ["a"]) + 0.5 + +To avoid underflow when working with many small score values it makes sense to +take their logarithm. +For convenience this can be done with the `logscore` method. + + >>> lm.logscore("a") + -2.700439718141092 + +Building on this method, we can also evaluate our model's cross-entropy and +perplexity with respect to sequences of ngrams. + + >>> test = [('a', 'b'), ('c', 'd')] + >>> lm.entropy(test) + 1.292481250360578 + >>> lm.perplexity(test) + 2.449489742783178 + +It is advisable to preprocess your test text exactly the same way as you did +the training text. + +One cool feature of ngram models is that they can be used to generate text. + + >>> lm.generate(1, random_seed=3) + '' + >>> lm.generate(5, random_seed=3) + ['', 'a', 'b', 'c', 'd'] + +Provide `random_seed` if you want to consistently reproduce the same text all +other things being equal. Here we are using it to test the examples. + +You can also condition your generation on some preceding text with the `context` +argument. + + >>> lm.generate(5, text_seed=['c'], random_seed=3) + ['', 'c', 'd', 'c', 'd'] + +Note that an ngram model is restricted in how much preceding context it can +take into account. For example, a trigram model can only condition its output +on 2 preceding words. If you pass in a 4-word context, the first two words +will be ignored. +""" + +from nltk.lm.counter import NgramCounter +from nltk.lm.models import ( + MLE, + AbsoluteDiscountingInterpolated, + KneserNeyInterpolated, + Laplace, + Lidstone, + StupidBackoff, + WittenBellInterpolated, +) +from nltk.lm.vocabulary import Vocabulary + +__all__ = [ + "Vocabulary", + "NgramCounter", + "MLE", + "Lidstone", + "Laplace", + "WittenBellInterpolated", + "KneserNeyInterpolated", + "AbsoluteDiscountingInterpolated", + "StupidBackoff", +] diff --git a/nltk/lm/api.py b/nltk/lm/api.py new file mode 100644 index 0000000..5bb3155 --- /dev/null +++ b/nltk/lm/api.py @@ -0,0 +1,239 @@ +# Natural Language Toolkit: Language Models +# +# Copyright (C) 2001-2026 NLTK Project +# Authors: Ilia Kurenkov +# URL: +# For license information, see LICENSE.TXT +"""Language Model Interface.""" + +import math +import random +import warnings +from abc import ABCMeta, abstractmethod +from bisect import bisect +from itertools import accumulate + +from nltk.lm.counter import NgramCounter +from nltk.lm.util import log_base2 +from nltk.lm.vocabulary import Vocabulary + + +class Smoothing(metaclass=ABCMeta): + """Ngram Smoothing Interface + + Implements Chen & Goodman 1995's idea that all smoothing algorithms have + certain features in common. This should ideally allow smoothing algorithms to + work both with Backoff and Interpolation. + """ + + def __init__(self, vocabulary, counter): + """ + :param vocabulary: The Ngram vocabulary object. + :type vocabulary: nltk.lm.vocab.Vocabulary + :param counter: The counts of the vocabulary items. + :type counter: nltk.lm.counter.NgramCounter + """ + self.vocab = vocabulary + self.counts = counter + + @abstractmethod + def unigram_score(self, word): + raise NotImplementedError() + + @abstractmethod + def alpha_gamma(self, word, context): + raise NotImplementedError() + + +def _mean(items): + """Return average (aka mean) for sequence of items.""" + return math.fsum(items) / len(items) + + +def _random_generator(seed_or_generator): + if isinstance(seed_or_generator, random.Random): + return seed_or_generator + return random.Random(seed_or_generator) + + +def _weighted_choice(population, weights, random_generator=None): + """Like random.choice, but with weights. + + Heavily inspired by python 3.6 `random.choices`. + """ + if not population: + raise ValueError("Can't choose from empty population") + if len(population) != len(weights): + raise ValueError("The number of weights does not match the population") + cum_weights = list(accumulate(weights)) + total = math.fsum(weights) + threshold = random_generator.random() + return population[bisect(cum_weights, total * threshold)] + + +class LanguageModel(metaclass=ABCMeta): + """ABC for Language Models. + + Cannot be directly instantiated itself. + + """ + + def __init__(self, order, vocabulary=None, counter=None): + """Creates new LanguageModel. + + :param vocabulary: If provided, this vocabulary will be used instead + of creating a new one when training. + :type vocabulary: `nltk.lm.Vocabulary` or None + :param counter: If provided, use this object to count ngrams. + :type counter: `nltk.lm.NgramCounter` or None + :param ngrams_fn: If given, defines how sentences in training text are turned to ngram + sequences. + :type ngrams_fn: function or None + :param pad_fn: If given, defines how sentences in training text are padded. + :type pad_fn: function or None + """ + self.order = order + if vocabulary and not isinstance(vocabulary, Vocabulary): + warnings.warn( + f"The `vocabulary` argument passed to {self.__class__.__name__!r} " + "must be an instance of `nltk.lm.Vocabulary`.", + stacklevel=3, + ) + self.vocab = Vocabulary() if vocabulary is None else vocabulary + self.counts = NgramCounter() if counter is None else counter + + def fit(self, text, vocabulary_text=None): + """Trains the model on a text. + + :param text: Training text as a sequence of sentences. + + """ + if not self.vocab: + if vocabulary_text is None: + raise ValueError( + "Cannot fit without a vocabulary or text to create it from." + ) + self.vocab.update(vocabulary_text) + self.counts.update(self.vocab.lookup(sent) for sent in text) + + def score(self, word, context=None): + """Masks out of vocab (OOV) words and computes their model score. + + For model-specific logic of calculating scores, see the `unmasked_score` + method. + """ + return self.unmasked_score( + self.vocab.lookup(word), self.vocab.lookup(context) if context else None + ) + + @abstractmethod + def unmasked_score(self, word, context=None): + """Score a word given some optional context. + + Concrete models are expected to provide an implementation. + Note that this method does not mask its arguments with the OOV label. + Use the `score` method for that. + + :param str word: Word for which we want the score + :param tuple(str) context: Context the word is in. + If `None`, compute unigram score. + :param context: tuple(str) or None + :rtype: float + """ + raise NotImplementedError() + + def logscore(self, word, context=None): + """Evaluate the log score of this word in this context. + + The arguments are the same as for `score` and `unmasked_score`. + + """ + return log_base2(self.score(word, context)) + + def context_counts(self, context): + """Helper method for retrieving counts for a given context. + + Assumes context has been checked and oov words in it masked. + :type context: tuple(str) or None + + """ + return ( + self.counts[len(context) + 1][context] if context else self.counts.unigrams + ) + + def entropy(self, text_ngrams): + """Calculate cross-entropy of model for given evaluation text. + + This implementation is based on the Shannon-McMillan-Breiman theorem, + as used and referenced by Dan Jurafsky and Jordan Boyd-Graber. + + :param Iterable(tuple(str)) text_ngrams: A sequence of ngram tuples. + :rtype: float + + """ + return -1 * _mean( + [self.logscore(ngram[-1], ngram[:-1]) for ngram in text_ngrams] + ) + + def perplexity(self, text_ngrams): + """Calculates the perplexity of the given text. + + This is simply 2 ** cross-entropy for the text, so the arguments are the same. + + """ + return pow(2.0, self.entropy(text_ngrams)) + + def generate(self, num_words=1, text_seed=None, random_seed=None): + """Generate words from the model. + + :param int num_words: How many words to generate. By default 1. + :param text_seed: Generation can be conditioned on preceding context. + :param random_seed: A random seed or an instance of `random.Random`. If provided, + makes the random sampling part of generation reproducible. + :return: One (str) word or a list of words generated from model. + + Examples: + + >>> from nltk.lm import MLE + >>> lm = MLE(2) + >>> lm.fit([[("a", "b"), ("b", "c")]], vocabulary_text=['a', 'b', 'c']) + >>> lm.fit([[("a",), ("b",), ("c",)]]) + >>> lm.generate(random_seed=3) + 'a' + >>> lm.generate(text_seed=['a']) + 'b' + + """ + text_seed = [] if text_seed is None else list(text_seed) + random_generator = _random_generator(random_seed) + # This is the base recursion case. + if num_words == 1: + context = ( + text_seed[-self.order + 1 :] + if len(text_seed) >= self.order + else text_seed + ) + samples = self.context_counts(self.vocab.lookup(context)) + while context and not samples: + context = context[1:] if len(context) > 1 else [] + samples = self.context_counts(self.vocab.lookup(context)) + # Sorting samples achieves two things: + # - reproducible randomness when sampling + # - turns Mapping into Sequence which `_weighted_choice` expects + samples = sorted(samples) + return _weighted_choice( + samples, + tuple(self.score(w, context) for w in samples), + random_generator, + ) + # We build up text one word at a time using the preceding context. + generated = [] + for _ in range(num_words): + generated.append( + self.generate( + num_words=1, + text_seed=text_seed + generated, + random_seed=random_generator, + ) + ) + return generated diff --git a/nltk/lm/counter.py b/nltk/lm/counter.py new file mode 100644 index 0000000..d918066 --- /dev/null +++ b/nltk/lm/counter.py @@ -0,0 +1,207 @@ +# Natural Language Toolkit +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ilia Kurenkov +# URL: +# For license information, see LICENSE.TXT +""" +Language Model Counter +---------------------- +""" + +from collections import defaultdict +from collections.abc import Sequence + +from nltk.probability import ConditionalFreqDist, FreqDist + +#: Upper bound on the number of *distinct* ngrams a single ``NgramCounter`` may +#: store. The counter keeps every distinct ngram of every order in a nested +#: dictionary tree, so training a language model on an untrusted corpus of +#: distinct tokens grows memory without limit (~order * tokens) and OOM-kills the +#: worker (CWE-770; CVE-2026-12928). Once this many distinct ngrams have been +#: stored, ``update`` raises ``ValueError`` instead of growing unbounded. The +#: default is generous enough for NLTK's documented training corpora (e.g. Brown +#: at the usual orders); raise it if you train on a genuinely larger corpus. +MAX_NGRAMS = 10_000_000 + + +class NgramCounter: + """Class for counting ngrams. + + Will count any ngram sequence you give it ;) + + First we need to make sure we are feeding the counter sentences of ngrams. + + >>> text = [["a", "b", "c", "d"], ["a", "c", "d", "c"]] + >>> from nltk.util import ngrams + >>> text_bigrams = [ngrams(sent, 2) for sent in text] + >>> text_unigrams = [ngrams(sent, 1) for sent in text] + + The counting itself is very simple. + + >>> from nltk.lm import NgramCounter + >>> ngram_counts = NgramCounter(text_bigrams + text_unigrams) + + You can conveniently access ngram counts using standard python dictionary notation. + String keys will give you unigram counts. + + >>> ngram_counts['a'] + 2 + >>> ngram_counts['aliens'] + 0 + + If you want to access counts for higher order ngrams, use a list or a tuple. + These are treated as "context" keys, so what you get is a frequency distribution + over all continuations after the given context. + + >>> sorted(ngram_counts[['a']].items()) + [('b', 1), ('c', 1)] + >>> sorted(ngram_counts[('a',)].items()) + [('b', 1), ('c', 1)] + + This is equivalent to specifying explicitly the order of the ngram (in this case + 2 for bigram) and indexing on the context. + + >>> ngram_counts[2][('a',)] is ngram_counts[['a']] + True + + Note that the keys in `ConditionalFreqDist` cannot be lists, only tuples! + It is generally advisable to use the less verbose and more flexible square + bracket notation. + + To get the count of the full ngram "a b", do this: + + >>> ngram_counts[['a']]['b'] + 1 + + Specifying the ngram order as a number can be useful for accessing all ngrams + in that order. + + >>> ngram_counts[2] + + + The keys of this `ConditionalFreqDist` are the contexts we discussed earlier. + Unigrams can also be accessed with a human-friendly alias. + + >>> ngram_counts.unigrams is ngram_counts[1] + True + + Similarly to `collections.Counter`, you can update counts after initialization. + + >>> ngram_counts['e'] + 0 + >>> ngram_counts.update([ngrams(["d", "e", "f"], 1)]) + >>> ngram_counts['e'] + 1 + + """ + + def __init__(self, ngram_text=None): + """Creates a new NgramCounter. + + If `ngram_text` is specified, counts ngrams from it, otherwise waits for + `update` method to be called explicitly. + + :param ngram_text: Optional text containing sentences of ngrams, as for `update` method. + :type ngram_text: Iterable(Iterable(tuple(str))) or None + + """ + self._counts = defaultdict(ConditionalFreqDist) + self._counts[1] = self.unigrams = FreqDist() + #: Number of distinct ngrams stored, tracked for the ``MAX_NGRAMS`` guard. + self._distinct = 0 + + if ngram_text: + self.update(ngram_text) + + def update(self, ngram_text): + """Updates ngram counts from `ngram_text`. + + Expects `ngram_text` to be a sequence of sentences (sequences). + Each sentence consists of ngrams as tuples of strings. + + :param Iterable(Iterable(tuple(str))) ngram_text: Text containing sentences of ngrams. + :raises TypeError: if the ngrams are not tuples. + + """ + + for sent in ngram_text: + for ngram in sent: + if not isinstance(ngram, tuple): + raise TypeError( + "Ngram <{}> isn't a tuple, " "but {}".format(ngram, type(ngram)) + ) + + ngram_order = len(ngram) + if ngram_order == 1: + if ngram[0] not in self.unigrams: + self._note_new_ngram() + self.unigrams[ngram[0]] += 1 + continue + + context, word = ngram[:-1], ngram[-1] + # Probe with .get() so testing whether this (context, word) pair + # is new does not eagerly create empty nested entries through the + # defaultdicts; otherwise an ngram refused by the MAX_NGRAMS guard + # would still leave new contexts/orders behind, defeating the + # memory bound. The real entries are created only once the guard + # below has passed. + order_counts = self._counts.get(ngram_order) + context_counts = ( + order_counts.get(context) if order_counts is not None else None + ) + if context_counts is None or word not in context_counts: + self._note_new_ngram() + self[ngram_order][context][word] += 1 + + def _note_new_ngram(self): + """Account for one newly-stored distinct ngram and enforce the bound. + + The counter retains every distinct ngram, so without a bound an untrusted + corpus of distinct tokens grows memory without limit and OOM-kills the + worker (CWE-770; CVE-2026-12928). Refuse once ``MAX_NGRAMS`` distinct + ngrams have been stored. The bound is checked *before* incrementing so a + refused ngram (which is never stored) does not inflate the counter. + """ + if self._distinct >= MAX_NGRAMS: + raise ValueError( + "Refusing to count further: NgramCounter exceeded the limit of " + "%d distinct ngrams (CWE-770). Training on this corpus would grow " + "memory without bound. Use a smaller corpus or order, or raise " + "nltk.lm.counter.MAX_NGRAMS." % MAX_NGRAMS + ) + self._distinct += 1 + + def N(self): + """Returns grand total number of ngrams stored. + + This includes ngrams from all orders, so some duplication is expected. + :rtype: int + + >>> from nltk.lm import NgramCounter + >>> counts = NgramCounter([[("a", "b"), ("c",), ("d", "e")]]) + >>> counts.N() + 3 + + """ + return sum(val.N() for val in self._counts.values()) + + def __getitem__(self, item): + """User-friendly access to ngram counts.""" + if isinstance(item, int): + return self._counts[item] + elif isinstance(item, str): + return self._counts.__getitem__(1)[item] + elif isinstance(item, Sequence): + return self._counts.__getitem__(len(item) + 1)[tuple(item)] + + def __str__(self): + return "<{} with {} ngram orders and {} ngrams>".format( + self.__class__.__name__, len(self._counts), self.N() + ) + + def __len__(self): + return self._counts.__len__() + + def __contains__(self, item): + return item in self._counts diff --git a/nltk/lm/models.py b/nltk/lm/models.py new file mode 100644 index 0000000..36dca02 --- /dev/null +++ b/nltk/lm/models.py @@ -0,0 +1,159 @@ +# Natural Language Toolkit: Language Models +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ilia Kurenkov +# Manu Joseph +# URL: +# For license information, see LICENSE.TXT +"""Language Models""" + +from nltk.lm.api import LanguageModel, Smoothing +from nltk.lm.smoothing import AbsoluteDiscounting, KneserNey, WittenBell + + +class MLE(LanguageModel): + """Class for providing MLE ngram model scores. + + Inherits initialization from BaseNgramModel. + """ + + def unmasked_score(self, word, context=None): + """Returns the MLE score for a word given a context. + + Args: + - word is expected to be a string + - context is expected to be something reasonably convertible to a tuple + """ + return self.context_counts(context).freq(word) + + +class Lidstone(LanguageModel): + """Provides Lidstone-smoothed scores. + + In addition to initialization arguments from BaseNgramModel also requires + a number by which to increase the counts, gamma. + """ + + def __init__(self, gamma, *args, **kwargs): + super().__init__(*args, **kwargs) + self.gamma = gamma + + def unmasked_score(self, word, context=None): + """Add-one smoothing: Lidstone or Laplace. + + To see what kind, look at `gamma` attribute on the class. + + """ + counts = self.context_counts(context) + word_count = counts[word] + norm_count = counts.N() + return (word_count + self.gamma) / (norm_count + len(self.vocab) * self.gamma) + + +class Laplace(Lidstone): + """Implements Laplace (add one) smoothing. + + Initialization identical to BaseNgramModel because gamma is always 1. + """ + + def __init__(self, *args, **kwargs): + super().__init__(1, *args, **kwargs) + + +class StupidBackoff(LanguageModel): + """Provides StupidBackoff scores. + + In addition to initialization arguments from BaseNgramModel also requires + a parameter alpha with which we scale the lower order probabilities. + Note that this is not a true probability distribution as scores for ngrams + of the same order do not sum up to unity. + """ + + def __init__(self, alpha=0.4, *args, **kwargs): + super().__init__(*args, **kwargs) + self.alpha = alpha + + def unmasked_score(self, word, context=None): + if context: + max_ctx = self.order - 1 + if max_ctx <= 0: + context = () + elif len(context) > max_ctx: + context = context[-max_ctx:] + + if not context: + # Base recursion + return self.counts.unigrams.freq(word) + + counts = self.context_counts(context) + word_count = counts[word] + norm_count = counts.N() + + if word_count > 0: + return word_count / norm_count + else: + return self.alpha * self.unmasked_score(word, context[1:]) + + +class InterpolatedLanguageModel(LanguageModel): + """Logic common to all interpolated language models. + + The idea to abstract this comes from Chen & Goodman 1995. + Do not instantiate this class directly! + """ + + def __init__(self, smoothing_cls, order, **kwargs): + params = kwargs.pop("params", {}) + super().__init__(order, **kwargs) + self.estimator = smoothing_cls(self.vocab, self.counts, **params) + + def unmasked_score(self, word, context=None): + if context: + max_ctx = self.order - 1 + if max_ctx <= 0: + context = () + elif len(context) > max_ctx: + context = context[-max_ctx:] + + if not context: + # The base recursion case: no context, we only have a unigram. + return self.estimator.unigram_score(word) + + if not self.counts[context]: + # It can also happen that we have no data for this context. + # In that case we defer to the lower-order ngram. + # This is the same as setting alpha to 0 and gamma to 1. + alpha, gamma = 0, 1 + else: + alpha, gamma = self.estimator.alpha_gamma(word, context) + + return alpha + gamma * self.unmasked_score(word, context[1:]) + + +class WittenBellInterpolated(InterpolatedLanguageModel): + """Interpolated version of Witten-Bell smoothing.""" + + def __init__(self, order, **kwargs): + super().__init__(WittenBell, order, **kwargs) + + +class AbsoluteDiscountingInterpolated(InterpolatedLanguageModel): + """Interpolated version of smoothing with absolute discount.""" + + def __init__(self, order, discount=0.75, **kwargs): + super().__init__( + AbsoluteDiscounting, order, params={"discount": discount}, **kwargs + ) + + +class KneserNeyInterpolated(InterpolatedLanguageModel): + """Interpolated version of Kneser-Ney smoothing.""" + + def __init__(self, order, discount=0.1, **kwargs): + if not (0 <= discount <= 1): + raise ValueError( + "Discount must be between 0 and 1 for probabilities to sum to unity." + ) + super().__init__( + KneserNey, order, params={"discount": discount, "order": order}, **kwargs + ) diff --git a/nltk/lm/preprocessing.py b/nltk/lm/preprocessing.py new file mode 100644 index 0000000..a251906 --- /dev/null +++ b/nltk/lm/preprocessing.py @@ -0,0 +1,51 @@ +# Natural Language Toolkit: Language Model Unit Tests +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ilia Kurenkov +# URL: +# For license information, see LICENSE.TXT +from functools import partial +from itertools import chain + +from nltk.util import everygrams, pad_sequence + +flatten = chain.from_iterable +pad_both_ends = partial( + pad_sequence, + pad_left=True, + left_pad_symbol="", + pad_right=True, + right_pad_symbol="", +) +pad_both_ends.__doc__ = """Pads both ends of a sentence to length specified by ngram order. + + Following convention pads the start of sentence pads its end. + """ + + +def padded_everygrams(order, sentence): + """Helper with some useful defaults. + + Applies pad_both_ends to sentence and follows it up with everygrams. + """ + return everygrams(list(pad_both_ends(sentence, n=order)), max_len=order) + + +def padded_everygram_pipeline(order, text): + """Default preprocessing for a sequence of sentences. + + Creates two iterators: + + - sentences padded and turned into sequences of `nltk.util.everygrams` + - sentences padded as above and chained together for a flat stream of words + + :param order: Largest ngram length produced by `everygrams`. + :param text: Text to iterate over. Expected to be an iterable of sentences. + :type text: Iterable[Iterable[str]] + :return: iterator over text as ngrams, iterator over text as vocabulary data + """ + padding_fn = partial(pad_both_ends, n=order) + return ( + (everygrams(list(padding_fn(sent)), max_len=order) for sent in text), + flatten(map(padding_fn, text)), + ) diff --git a/nltk/lm/smoothing.py b/nltk/lm/smoothing.py new file mode 100644 index 0000000..73d68d0 --- /dev/null +++ b/nltk/lm/smoothing.py @@ -0,0 +1,127 @@ +# Natural Language Toolkit: Language Model Unit Tests +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ilia Kurenkov +# Manu Joseph +# URL: +# For license information, see LICENSE.TXT +"""Smoothing algorithms for language modeling. + +According to Chen & Goodman 1995 these should work with both Backoff and +Interpolation. +""" +from operator import methodcaller + +from nltk.lm.api import Smoothing +from nltk.probability import ConditionalFreqDist + + +def _count_values_gt_zero(distribution): + """Count values that are greater than zero in a distribution. + + Assumes distribution is either a mapping with counts as values or + an instance of `nltk.ConditionalFreqDist`. + """ + as_count = ( + methodcaller("N") + if isinstance(distribution, ConditionalFreqDist) + else lambda count: count + ) + # We explicitly check that values are > 0 to guard against negative counts. + return sum( + 1 for dist_or_count in distribution.values() if as_count(dist_or_count) > 0 + ) + + +class WittenBell(Smoothing): + """Witten-Bell smoothing.""" + + def __init__(self, vocabulary, counter, **kwargs): + super().__init__(vocabulary, counter, **kwargs) + + def alpha_gamma(self, word, context): + alpha = self.counts[context].freq(word) + gamma = self._gamma(context) + return (1.0 - gamma) * alpha, gamma + + def _gamma(self, context): + n_plus = _count_values_gt_zero(self.counts[context]) + return n_plus / (n_plus + self.counts[context].N()) + + def unigram_score(self, word): + return self.counts.unigrams.freq(word) + + +class AbsoluteDiscounting(Smoothing): + """Smoothing with absolute discount.""" + + def __init__(self, vocabulary, counter, discount=0.75, **kwargs): + super().__init__(vocabulary, counter, **kwargs) + self.discount = discount + + def alpha_gamma(self, word, context): + alpha = ( + max(self.counts[context][word] - self.discount, 0) + / self.counts[context].N() + ) + gamma = self._gamma(context) + return alpha, gamma + + def _gamma(self, context): + n_plus = _count_values_gt_zero(self.counts[context]) + return (self.discount * n_plus) / self.counts[context].N() + + def unigram_score(self, word): + return self.counts.unigrams.freq(word) + + +class KneserNey(Smoothing): + """Kneser-Ney Smoothing. + + This is an extension of smoothing with a discount. + + Resources: + - https://pages.ucsd.edu/~rlevy/lign256/winter2008/kneser_ney_mini_example.pdf + - https://www.youtube.com/watch?v=ody1ysUTD7o + - https://medium.com/@dennyc/a-simple-numerical-example-for-kneser-ney-smoothing-nlp-4600addf38b8 + - https://www.cl.uni-heidelberg.de/courses/ss15/smt/scribe6.pdf + - https://www-i6.informatik.rwth-aachen.de/publications/download/951/Kneser-ICASSP-1995.pdf + """ + + def __init__(self, vocabulary, counter, order, discount=0.1, **kwargs): + super().__init__(vocabulary, counter, **kwargs) + self.discount = discount + self._order = order + + def unigram_score(self, word): + word_continuation_count, total_count = self._continuation_counts(word) + return word_continuation_count / total_count + + def alpha_gamma(self, word, context): + prefix_counts = self.counts[context] + word_continuation_count, total_count = ( + (prefix_counts[word], prefix_counts.N()) + if len(context) + 1 == self._order + else self._continuation_counts(word, context) + ) + alpha = max(word_continuation_count - self.discount, 0.0) / total_count + gamma = self.discount * _count_values_gt_zero(prefix_counts) / total_count + return alpha, gamma + + def _continuation_counts(self, word, context=tuple()): + """Count continuations that end with context and word. + + Continuations track unique ngram "types", regardless of how many + instances were observed for each "type". + This is different than raw ngram counts which track number of instances. + """ + higher_order_ngrams_with_context = ( + counts + for prefix_ngram, counts in self.counts[len(context) + 2].items() + if prefix_ngram[1:] == context + ) + higher_order_ngrams_with_word_count, total = 0, 0 + for counts in higher_order_ngrams_with_context: + higher_order_ngrams_with_word_count += int(counts[word] > 0) + total += _count_values_gt_zero(counts) + return higher_order_ngrams_with_word_count, total diff --git a/nltk/lm/util.py b/nltk/lm/util.py new file mode 100644 index 0000000..670b205 --- /dev/null +++ b/nltk/lm/util.py @@ -0,0 +1,19 @@ +# Natural Language Toolkit +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ilia Kurenkov +# URL: +# For license information, see LICENSE.TXT +"""Language Model Utilities""" + +from math import log + +NEG_INF = float("-inf") +POS_INF = float("inf") + + +def log_base2(score): + """Convenience function for computing logarithms with base 2.""" + if score == 0.0: + return NEG_INF + return log(score, 2) diff --git a/nltk/lm/vocabulary.py b/nltk/lm/vocabulary.py new file mode 100644 index 0000000..8d0c2dd --- /dev/null +++ b/nltk/lm/vocabulary.py @@ -0,0 +1,218 @@ +# Natural Language Toolkit +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ilia Kurenkov +# URL: +# For license information, see LICENSE.TXT +"""Language Model Vocabulary""" + +import sys +from collections import Counter +from collections.abc import Iterable +from functools import singledispatch +from itertools import chain + + +@singledispatch +def _dispatched_lookup(words, vocab): + raise TypeError(f"Unsupported type for looking up in vocabulary: {type(words)}") + + +@_dispatched_lookup.register(Iterable) +def _(words, vocab): + """Look up a sequence of words in the vocabulary. + + Returns an iterator over looked up words. + + """ + return tuple(_dispatched_lookup(w, vocab) for w in words) + + +@_dispatched_lookup.register(str) +def _string_lookup(word, vocab): + """Looks up one word in the vocabulary.""" + return word if word in vocab else vocab.unk_label + + +class Vocabulary: + """Stores language model vocabulary. + + Satisfies two common language modeling requirements for a vocabulary: + + - When checking membership and calculating its size, filters items + by comparing their counts to a cutoff value. + - Adds a special "unknown" token which unseen words are mapped to. + + >>> words = ['a', 'c', '-', 'd', 'c', 'a', 'b', 'r', 'a', 'c', 'd'] + >>> from nltk.lm import Vocabulary + >>> vocab = Vocabulary(words, unk_cutoff=2) + + Tokens with counts greater than or equal to the cutoff value will + be considered part of the vocabulary. + + >>> vocab['c'] + 3 + >>> 'c' in vocab + True + >>> vocab['d'] + 2 + >>> 'd' in vocab + True + + Tokens with frequency counts less than the cutoff value will be considered not + part of the vocabulary even though their entries in the count dictionary are + preserved. + + >>> vocab['b'] + 1 + >>> 'b' in vocab + False + >>> vocab['aliens'] + 0 + >>> 'aliens' in vocab + False + + Keeping the count entries for seen words allows us to change the cutoff value + without having to recalculate the counts. + + >>> vocab2 = Vocabulary(vocab.counts, unk_cutoff=1) + >>> "b" in vocab2 + True + + The cutoff value influences not only membership checking but also the result of + getting the size of the vocabulary using the built-in `len`. + Note that while the number of keys in the vocabulary's counter stays the same, + the items in the vocabulary differ depending on the cutoff. + We use `sorted` to demonstrate because it keeps the order consistent. + + >>> sorted(vocab2.counts) + ['-', 'a', 'b', 'c', 'd', 'r'] + >>> sorted(vocab2) + ['-', '', 'a', 'b', 'c', 'd', 'r'] + >>> sorted(vocab.counts) + ['-', 'a', 'b', 'c', 'd', 'r'] + >>> sorted(vocab) + ['', 'a', 'c', 'd'] + + In addition to items it gets populated with, the vocabulary stores a special + token that stands in for so-called "unknown" items. By default it's "". + + >>> "" in vocab + True + + We can look up words in a vocabulary using its `lookup` method. + "Unseen" words (with counts less than cutoff) are looked up as the unknown label. + If given one word (a string) as an input, this method will return a string. + + >>> vocab.lookup("a") + 'a' + >>> vocab.lookup("aliens") + '' + + If given a sequence, it will return an tuple of the looked up words. + + >>> vocab.lookup(["p", 'a', 'r', 'd', 'b', 'c']) + ('', 'a', '', 'd', '', 'c') + + It's possible to update the counts after the vocabulary has been created. + In general, the interface is the same as that of `collections.Counter`. + + >>> vocab['b'] + 1 + >>> vocab.update(["b", "b", "c"]) + >>> vocab['b'] + 3 + """ + + def __init__(self, counts=None, unk_cutoff=1, unk_label=""): + """Create a new Vocabulary. + + :param counts: Optional iterable or `collections.Counter` instance to + pre-seed the Vocabulary. In case it is iterable, counts + are calculated. + :param int unk_cutoff: Words that occur less frequently than this value + are not considered part of the vocabulary. + :param unk_label: Label for marking words not part of vocabulary. + + """ + self.unk_label = unk_label + if unk_cutoff < 1: + raise ValueError(f"Cutoff value cannot be less than 1. Got: {unk_cutoff}") + self._cutoff = unk_cutoff + + self.counts = Counter() + self.update(counts if counts is not None else "") + + @property + def cutoff(self): + """Cutoff value. + + Items with count below this value are not considered part of vocabulary. + + """ + return self._cutoff + + def update(self, *counter_args, **counter_kwargs): + """Update vocabulary counts. + + Wraps `collections.Counter.update` method. + + """ + self.counts.update(*counter_args, **counter_kwargs) + self._len = sum(1 for _ in self) + + def lookup(self, words): + """Look up one or more words in the vocabulary. + + If passed one word as a string will return that word or `self.unk_label`. + Otherwise will assume it was passed a sequence of words, will try to look + each of them up and return an iterator over the looked up words. + + :param words: Word(s) to look up. + :type words: Iterable(str) or str + :rtype: generator(str) or str + :raises: TypeError for types other than strings or iterables + + >>> from nltk.lm import Vocabulary + >>> vocab = Vocabulary(["a", "b", "c", "a", "b"], unk_cutoff=2) + >>> vocab.lookup("a") + 'a' + >>> vocab.lookup("aliens") + '' + >>> vocab.lookup(["a", "b", "c", ["x", "b"]]) + ('a', 'b', '', ('', 'b')) + + """ + return _dispatched_lookup(words, self) + + def __getitem__(self, item): + return self._cutoff if item == self.unk_label else self.counts[item] + + def __contains__(self, item): + """Only consider items with counts GE to cutoff as being in the + vocabulary.""" + return self[item] >= self.cutoff + + def __iter__(self): + """Building on membership check define how to iterate over + vocabulary.""" + return chain( + (item for item in self.counts if item in self), + [self.unk_label] if self.counts else [], + ) + + def __len__(self): + """Computing size of vocabulary reflects the cutoff.""" + return self._len + + def __eq__(self, other): + return ( + self.unk_label == other.unk_label + and self.cutoff == other.cutoff + and self.counts == other.counts + ) + + def __str__(self): + return "<{} with cutoff={} unk_label='{}' and {} items>".format( + self.__class__.__name__, self.cutoff, self.unk_label, len(self) + ) diff --git a/nltk/metrics/__init__.py b/nltk/metrics/__init__.py new file mode 100644 index 0000000..28a7405 --- /dev/null +++ b/nltk/metrics/__init__.py @@ -0,0 +1,51 @@ +# Natural Language Toolkit: Metrics +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT +# + +""" +NLTK Metrics + +Classes and methods for scoring processing modules. +""" + +from nltk.metrics.agreement import AnnotationTask +from nltk.metrics.aline import align +from nltk.metrics.association import ( + BigramAssocMeasures, + ContingencyMeasures, + NgramAssocMeasures, + QuadgramAssocMeasures, + TrigramAssocMeasures, +) +from nltk.metrics.confusionmatrix import ConfusionMatrix +from nltk.metrics.distance import ( + binary_distance, + custom_distance, + edit_distance, + edit_distance_align, + fractional_presence, + interval_distance, + jaccard_distance, + masi_distance, + presence, +) +from nltk.metrics.paice import Paice +from nltk.metrics.scores import ( + accuracy, + approxrand, + f_measure, + log_likelihood, + precision, + recall, +) +from nltk.metrics.segmentation import ghd, pk, windowdiff +from nltk.metrics.spearman import ( + ranks_from_scores, + ranks_from_sequence, + spearman_correlation, +) diff --git a/nltk/metrics/agreement.py b/nltk/metrics/agreement.py new file mode 100644 index 0000000..d932228 --- /dev/null +++ b/nltk/metrics/agreement.py @@ -0,0 +1,487 @@ +# Natural Language Toolkit: Agreement Metrics +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Tom Lippincott +# URL: +# For license information, see LICENSE.TXT +# + +""" +Implementations of inter-annotator agreement coefficients surveyed by Artstein +and Poesio (2007), Inter-Coder Agreement for Computational Linguistics. + +An agreement coefficient calculates the amount that annotators agreed on label +assignments beyond what is expected by chance. + +In defining the AnnotationTask class, we use naming conventions similar to the +paper's terminology. There are three types of objects in an annotation task: + + the coders (variables "c" and "C") + the items to be annotated (variables "i" and "I") + the potential categories to be assigned (variables "k" and "K") + +Additionally, it is often the case that we don't want to treat two different +labels as complete disagreement, and so the AnnotationTask constructor can also +take a distance metric as a final argument. Distance metrics are simply +functions that take two arguments, and return a value between 0.0 and 1.0 +indicating the distance between them. If not supplied, the default is binary +comparison between the arguments. + +The simplest way to initialize an AnnotationTask is with a list of triples, +each containing a coder's assignment for one object in the task: + + task = AnnotationTask(data=[('c1', '1', 'v1'),('c2', '1', 'v1'),...]) + +Note that the data list needs to contain the same number of triples for each +individual coder, containing category values for the same set of items. + +Alpha (Krippendorff 1980) +Kappa (Cohen 1960) +S (Bennet, Albert and Goldstein 1954) +Pi (Scott 1955) + + +TODO: Describe handling of multiple coders and missing data + +Expected results from the Artstein and Poesio survey paper: + + >>> from nltk.metrics.agreement import AnnotationTask + >>> import os.path + >>> t = AnnotationTask(data=[x.split() for x in open(os.path.join(os.path.dirname(__file__), "artstein_poesio_example.txt"))]) + >>> t.avg_Ao() + 0.88 + >>> round(t.pi(), 5) + 0.79953 + >>> round(t.S(), 2) + 0.82 + + This would have returned a wrong value (0.0) in @785fb79 as coders are in + the wrong order. Subsequently, all values for pi(), S(), and kappa() would + have been wrong as they are computed with avg_Ao(). + >>> t2 = AnnotationTask(data=[('b','1','stat'),('a','1','stat')]) + >>> t2.avg_Ao() + 1.0 + + The following, of course, also works. + >>> t3 = AnnotationTask(data=[('a','1','othr'),('b','1','othr')]) + >>> t3.avg_Ao() + 1.0 + +""" + +import logging +import math +from itertools import groupby +from operator import itemgetter + +from nltk.internals import deprecated +from nltk.metrics.distance import binary_distance +from nltk.probability import ConditionalFreqDist, FreqDist + +log = logging.getLogger(__name__) + + +class AnnotationTask: + """Represents an annotation task, i.e. people assign labels to items. + + Notation tries to match notation in Artstein and Poesio (2007). + + In general, coders and items can be represented as any hashable object. + Integers, for example, are fine, though strings are more readable. + Labels must support the distance functions applied to them, so e.g. + a string-edit-distance makes no sense if your labels are integers, + whereas interval distance needs numeric values. A notable case of this + is the MASI metric, which requires Python sets. + """ + + def __init__(self, data=None, distance=binary_distance): + """Initialize an annotation task. + + The data argument can be None (to create an empty annotation task) or a sequence of 3-tuples, + each representing a coder's labeling of an item: + ``(coder,item,label)`` + + The distance argument is a function taking two arguments (labels) and producing a numerical distance. + The distance from a label to itself should be zero: + ``distance(l,l) = 0`` + """ + self.distance = distance + self.I = set() + self.K = set() + self.C = set() + self.data = [] + if data is not None: + self.load_array(data) + + def __str__(self): + return "\r\n".join( + map( + lambda x: "%s\t%s\t%s" + % (x["coder"], x["item"].replace("_", "\t"), ",".join(x["labels"])), + self.data, + ) + ) + + def load_array(self, array): + """Load an sequence of annotation results, appending to any data already loaded. + + The argument is a sequence of 3-tuples, each representing a coder's labeling of an item: + (coder,item,label) + """ + for coder, item, labels in array: + self.C.add(coder) + self.K.add(labels) + self.I.add(item) + self.data.append({"coder": coder, "labels": labels, "item": item}) + + def agr(self, cA, cB, i, data=None): + """Agreement between two coders on a given item""" + data = data or self.data + # cfedermann: we don't know what combination of coder/item will come + # first in x; to avoid StopIteration problems due to assuming an order + # cA,cB, we allow either for k1 and then look up the missing as k2. + k1 = next(x for x in data if x["coder"] in (cA, cB) and x["item"] == i) + if k1["coder"] == cA: + k2 = next(x for x in data if x["coder"] == cB and x["item"] == i) + else: + k2 = next(x for x in data if x["coder"] == cA and x["item"] == i) + + ret = 1.0 - float(self.distance(k1["labels"], k2["labels"])) + log.debug("Observed agreement between %s and %s on %s: %f", cA, cB, i, ret) + log.debug( + 'Distance between "%r" and "%r": %f', k1["labels"], k2["labels"], 1.0 - ret + ) + return ret + + def Nk(self, k): + return float(sum(1 for x in self.data if x["labels"] == k)) + + def Nik(self, i, k): + return float(sum(1 for x in self.data if x["item"] == i and x["labels"] == k)) + + def Nck(self, c, k): + return float(sum(1 for x in self.data if x["coder"] == c and x["labels"] == k)) + + @deprecated("Use Nk, Nik or Nck instead") + def N(self, k=None, i=None, c=None): + """Implements the "n-notation" used in Artstein and Poesio (2007)""" + if k is not None and i is None and c is None: + ret = self.Nk(k) + elif k is not None and i is not None and c is None: + ret = self.Nik(i, k) + elif k is not None and c is not None and i is None: + ret = self.Nck(c, k) + else: + raise ValueError( + f"You must pass either i or c, not both! (k={k!r},i={i!r},c={c!r})" + ) + log.debug("Count on N[%s,%s,%s]: %d", k, i, c, ret) + return ret + + def _grouped_data(self, field, data=None): + data = data or self.data + return groupby(sorted(data, key=itemgetter(field)), itemgetter(field)) + + def Ao(self, cA, cB): + """Observed agreement between two coders on all items.""" + data = self._grouped_data( + "item", (x for x in self.data if x["coder"] in (cA, cB)) + ) + ret = sum(self.agr(cA, cB, item, item_data) for item, item_data in data) / len( + self.I + ) + log.debug("Observed agreement between %s and %s: %f", cA, cB, ret) + return ret + + def _pairwise_average(self, function): + """ + Calculates the average of function results for each coder pair + """ + total = 0 + n = 0 + s = self.C.copy() + for cA in self.C: + s.remove(cA) + for cB in s: + total += function(cA, cB) + n += 1 + ret = total / n + return ret + + def _chance_corrected_agreement(self, observed, expected): + """Handle degenerate perfect-agreement cases consistently. + + When expected agreement is 1.0 and observed agreement is also 1.0, + returns 1.0 (perfect agreement). Raises ValueError if expected is 1.0 + but observed is not, since that indicates a violated distance contract + (distance(l, l) must be 0) or otherwise undefined coefficient semantics. + """ + if math.isclose(expected, 1.0): + if math.isclose(observed, 1.0): + return 1.0 + raise ValueError( + f"Expected agreement is 1.0 but observed agreement is {observed:.4f}. " + "This indicates a distance function that violates distance(l, l) = 0, " + "or otherwise undefined coefficient semantics." + ) + return (observed - expected) / (1.0 - expected) + + def avg_Ao(self): + """Average observed agreement across all coders and items.""" + ret = self._pairwise_average(self.Ao) + log.debug("Average observed agreement: %f", ret) + return ret + + def Do_Kw_pairwise(self, cA, cB, max_distance=1.0): + """The observed disagreement for the weighted kappa coefficient.""" + total = 0.0 + data = (x for x in self.data if x["coder"] in (cA, cB)) + for i, itemdata in self._grouped_data("item", data): + # we should have two items; distance doesn't care which comes first + total += self.distance(next(itemdata)["labels"], next(itemdata)["labels"]) + + ret = total / (len(self.I) * max_distance) + log.debug("Observed disagreement between %s and %s: %f", cA, cB, ret) + return ret + + def Do_Kw(self, max_distance=1.0): + """Averaged over all labelers""" + ret = self._pairwise_average( + lambda cA, cB: self.Do_Kw_pairwise(cA, cB, max_distance) + ) + log.debug("Observed disagreement: %f", ret) + return ret + + # Agreement Coefficients + def S(self): + """Bennett, Albert and Goldstein 1954""" + if len(self.K) == 0: + raise ValueError("Cannot calculate S, no data present!") + Ae = 1.0 / len(self.K) + return self._chance_corrected_agreement(self.avg_Ao(), Ae) + + def pi(self): + """Scott 1955; here, multi-pi. + Equivalent to K from Siegel and Castellan (1988). + + """ + total = 0.0 + label_freqs = FreqDist(x["labels"] for x in self.data) + for k, f in label_freqs.items(): + total += f**2 + Ae = total / ((len(self.I) * len(self.C)) ** 2) + return self._chance_corrected_agreement(self.avg_Ao(), Ae) + + def Ae_kappa(self, cA, cB): + Ae = 0.0 + nitems = float(len(self.I)) + label_freqs = ConditionalFreqDist((x["labels"], x["coder"]) for x in self.data) + for k in label_freqs.conditions(): + Ae += (label_freqs[k][cA] / nitems) * (label_freqs[k][cB] / nitems) + return Ae + + def kappa_pairwise(self, cA, cB): + """ """ + Ae = self.Ae_kappa(cA, cB) + ret = self._chance_corrected_agreement(self.Ao(cA, cB), Ae) + log.debug("Expected agreement between %s and %s: %f", cA, cB, Ae) + return ret + + def kappa(self): + """Cohen 1960 + Averages naively over kappas for each coder pair. + + """ + return self._pairwise_average(self.kappa_pairwise) + + def multi_kappa(self): + """Davies and Fleiss 1982 + Averages over observed and expected agreements for each coder pair. + + """ + Ae = self._pairwise_average(self.Ae_kappa) + return self._chance_corrected_agreement(self.avg_Ao(), Ae) + + def Disagreement(self, label_freqs): + total_labels = sum(label_freqs.values()) + pairs = 0.0 + for j, nj in label_freqs.items(): + for l, nl in label_freqs.items(): + pairs += float(nj * nl) * self.distance(l, j) + return 1.0 * pairs / (total_labels * (total_labels - 1)) + + def alpha(self): + """Krippendorff 1980""" + # check for degenerate cases + if len(self.K) == 0: + raise ValueError("Cannot calculate alpha, no data present!") + if len(self.K) == 1: + log.debug("Only one annotation value, alpha returning 1.") + return 1 + if len(self.C) == 1 and len(self.I) == 1: + raise ValueError("Cannot calculate alpha, only one coder and item present!") + + total_disagreement = 0.0 + total_ratings = 0 + all_valid_labels_freq = FreqDist([]) + total_do = 0.0 # Total observed disagreement for all items. + for i, itemdata in self._grouped_data("item"): + label_freqs = FreqDist(x["labels"] for x in itemdata) + labels_count = sum(label_freqs.values()) + if labels_count < 2: + # Ignore the item. + continue + all_valid_labels_freq += label_freqs + total_do += self.Disagreement(label_freqs) * labels_count + + if len(all_valid_labels_freq.keys()) == 1: + log.debug("Only one valid annotation value, alpha returning 1.") + return 1 + + do = total_do / sum(all_valid_labels_freq.values()) + + de = self.Disagreement(all_valid_labels_freq) # Expected disagreement. + k_alpha = 1.0 - do / de + + return k_alpha + + def weighted_kappa_pairwise(self, cA, cB, max_distance=1.0): + """Cohen 1968""" + total = 0.0 + label_freqs = ConditionalFreqDist( + (x["coder"], x["labels"]) for x in self.data if x["coder"] in (cA, cB) + ) + for j in self.K: + for l in self.K: + total += label_freqs[cA][j] * label_freqs[cB][l] * self.distance(j, l) + De = total / (max_distance * pow(len(self.I), 2)) + log.debug("Expected disagreement between %s and %s: %f", cA, cB, De) + Do = self.Do_Kw_pairwise(cA, cB) + ret = 1.0 - (Do / De) + return ret + + def weighted_kappa(self, max_distance=1.0): + """Cohen 1968""" + return self._pairwise_average( + lambda cA, cB: self.weighted_kappa_pairwise(cA, cB, max_distance) + ) + + +if __name__ == "__main__": + import optparse + import re + + from nltk.metrics import distance + + # process command-line arguments + parser = optparse.OptionParser() + parser.add_option( + "-d", + "--distance", + dest="distance", + default="binary_distance", + help="distance metric to use", + ) + parser.add_option( + "-a", + "--agreement", + dest="agreement", + default="kappa", + help="agreement coefficient to calculate", + ) + parser.add_option( + "-e", + "--exclude", + dest="exclude", + action="append", + default=[], + help="coder names to exclude (may be specified multiple times)", + ) + parser.add_option( + "-i", + "--include", + dest="include", + action="append", + default=[], + help="coder names to include, same format as exclude", + ) + parser.add_option( + "-f", + "--file", + dest="file", + help="file to read labelings from, each line with three columns: 'labeler item labels'", + ) + parser.add_option( + "-v", + "--verbose", + dest="verbose", + default="0", + help="how much debugging to print on stderr (0-4)", + ) + parser.add_option( + "-c", + "--columnsep", + dest="columnsep", + default="\t", + help="char/string that separates the three columns in the file, defaults to tab", + ) + parser.add_option( + "-l", + "--labelsep", + dest="labelsep", + default=",", + help="char/string that separates labels (if labelers can assign more than one), defaults to comma", + ) + parser.add_option( + "-p", + "--presence", + dest="presence", + default=None, + help="convert each labeling into 1 or 0, based on presence of LABEL", + ) + parser.add_option( + "-T", + "--thorough", + dest="thorough", + default=False, + action="store_true", + help="calculate agreement for every subset of the annotators", + ) + options, remainder = parser.parse_args() + + if not options.file: + parser.print_help() + exit() + + logging.basicConfig(level=50 - 10 * int(options.verbose)) + + # read in data from the specified file + data = [] + with open(options.file) as infile: + for l in infile: + toks = l.split(options.columnsep) + coder, object_, labels = ( + toks[0], + str(toks[1:-1]), + frozenset(toks[-1].strip().split(options.labelsep)), + ) + if ( + (options.include == options.exclude) + or (len(options.include) > 0 and coder in options.include) + or (len(options.exclude) > 0 and coder not in options.exclude) + ): + data.append((coder, object_, labels)) + + if options.presence: + task = AnnotationTask( + data, getattr(distance, options.distance)(options.presence) + ) + else: + task = AnnotationTask(data, getattr(distance, options.distance)) + + if options.thorough: + pass + else: + print(getattr(task, options.agreement)()) + + logging.shutdown() diff --git a/nltk/metrics/aline.py b/nltk/metrics/aline.py new file mode 100644 index 0000000..b1a0f3a --- /dev/null +++ b/nltk/metrics/aline.py @@ -0,0 +1,1652 @@ +# Natural Language Toolkit: ALINE +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Greg Kondrak +# Geoff Bacon (Python port) +# URL: +# For license information, see LICENSE.TXT + +""" +ALINE +https://webdocs.cs.ualberta.ca/~kondrak/ +Copyright 2002 by Grzegorz Kondrak. + +ALINE is an algorithm for aligning phonetic sequences, described in [1]. +This module is a port of Kondrak's (2002) ALINE. It provides functions for +phonetic sequence alignment and similarity analysis. These are useful in +historical linguistics, sociolinguistics and synchronic phonology. + +ALINE has parameters that can be tuned for desired output. These parameters are: +- C_skip, C_sub, C_exp, C_vwl +- Salience weights +- Segmental features + +In this implementation, some parameters have been changed from their default +values as described in [1], in order to replicate published results. All changes +are noted in comments. + +Example usage +------------- + +# Get optimal alignment of two phonetic sequences + +>>> align('θin', 'tenwis') # doctest: +SKIP +[[('θ', 't'), ('i', 'e'), ('n', 'n'), ('-', 'w'), ('-', 'i'), ('-', 's')]] + +[1] G. Kondrak. Algorithms for Language Reconstruction. PhD dissertation, +University of Toronto. +""" + +try: + import numpy as np +except ImportError: + np = None + +# === Constants === + +inf = float("inf") + +# Default values for maximum similarity scores (Kondrak 2002: 54) +C_skip = -10 # Indels +C_sub = 35 # Substitutions +C_exp = 45 # Expansions/compressions +C_vwl = 5 # Vowel/consonant relative weight (decreased from 10) + +consonants = [ + "B", + "N", + "R", + "b", + "c", + "d", + "f", + "g", + "h", + "j", + "k", + "l", + "m", + "n", + "p", + "q", + "r", + "s", + "t", + "v", + "x", + "z", + "ç", + "ð", + "ħ", + "ŋ", + "ɖ", + "ɟ", + "ɢ", + "ɣ", + "ɦ", + "ɬ", + "ɮ", + "ɰ", + "ɱ", + "ɲ", + "ɳ", + "ɴ", + "ɸ", + "ɹ", + "ɻ", + "ɽ", + "ɾ", + "ʀ", + "ʁ", + "ʂ", + "ʃ", + "ʈ", + "ʋ", + "ʒ", + "ʔ", + "ʕ", + "ʙ", + "ʝ", + "β", + "θ", + "χ", + "ʐ", + "w", +] + +vowels = [ + "A", + "E", + "I", + "O", + "U", + "a", + "e", + "e̞", + "i", + "o", + "o̞", + "u", + "y", + "ä", + "æ", + "ø", + "ø̞", + "œ", + "ɐ", + "ɑ", + "ɒ", + "ɔ", + "ɘ", + "ə", + "ɛ", + "ɜ", + "ɞ", + "ɤ", + "ɤ̞", + "ɨ", + "ɯ", + "ɵ", + "ɶ", + "ʉ", + "ʊ", + "ʌ", + "ʏ", +] + +# Relevant features for comparing consonants and vowels +R_c = [ + "aspirated", + "lateral", + "manner", + "nasal", + "place", + "retroflex", + "syllabic", + "voice", +] +# 'high' taken out of R_v because same as manner +R_v = [ + "back", + "lateral", + "long", + "manner", + "nasal", + "place", + "retroflex", + "round", + "syllabic", + "voice", +] + +# Flattened feature matrix (Kondrak 2002: 56) +similarity_matrix = { + # place + "bilabial": 1.0, + "labiodental": 0.95, + "dental": 0.9, + "alveolar": 0.85, + "retroflex": 0.8, + "palato-alveolar": 0.75, + "palatal": 0.7, + "velar": 0.6, + "uvular": 0.5, + "pharyngeal": 0.3, + "glottal": 0.1, + "labiovelar": 1.0, + "vowel": -1.0, # added 'vowel' + # manner + "stop": 1.0, + "affricate": 0.9, + "fricative": 0.85, # increased fricative from 0.8 + "trill": 0.7, + "tap": 0.65, + "approximant": 0.6, + "high vowel": 0.4, + "mid vowel": 0.2, + "low vowel": 0.0, + "vowel2": 0.5, # added vowel + # high + "high": 1.0, + "mid": 0.5, + "low": 0.0, + # back + "front": 1.0, + "central": 0.5, + "back": 0.0, + # binary features + "plus": 1.0, + "minus": 0.0, +} + +# Relative weights of phonetic features (Kondrak 2002: 55) +salience = { + "syllabic": 5, + "place": 40, + "manner": 50, + "voice": 5, # decreased from 10 + "nasal": 20, # increased from 10 + "retroflex": 10, + "lateral": 10, + "aspirated": 5, + "long": 0, # decreased from 1 + "high": 3, # decreased from 5 + "back": 2, # decreased from 5 + "round": 2, # decreased from 5 +} + +# (Kondrak 2002: 59-60) +feature_matrix = { + # Consonants + "p": { + "place": "bilabial", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "b": { + "place": "bilabial", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "t": { + "place": "alveolar", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "d": { + "place": "alveolar", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʈ": { + "place": "retroflex", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɖ": { + "place": "retroflex", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "c": { + "place": "palatal", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɟ": { + "place": "palatal", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "k": { + "place": "velar", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "g": { + "place": "velar", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "q": { + "place": "uvular", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɢ": { + "place": "uvular", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʔ": { + "place": "glottal", + "manner": "stop", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "m": { + "place": "bilabial", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɱ": { + "place": "labiodental", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "n": { + "place": "alveolar", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɳ": { + "place": "retroflex", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɲ": { + "place": "palatal", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ŋ": { + "place": "velar", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɴ": { + "place": "uvular", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "N": { + "place": "uvular", + "manner": "stop", + "syllabic": "minus", + "voice": "plus", + "nasal": "plus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʙ": { + "place": "bilabial", + "manner": "trill", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "B": { + "place": "bilabial", + "manner": "trill", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "r": { + "place": "alveolar", + "manner": "trill", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʀ": { + "place": "uvular", + "manner": "trill", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "R": { + "place": "uvular", + "manner": "trill", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɾ": { + "place": "alveolar", + "manner": "tap", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɽ": { + "place": "retroflex", + "manner": "tap", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɸ": { + "place": "bilabial", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "β": { + "place": "bilabial", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "f": { + "place": "labiodental", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "v": { + "place": "labiodental", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "θ": { + "place": "dental", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ð": { + "place": "dental", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "s": { + "place": "alveolar", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "z": { + "place": "alveolar", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʃ": { + "place": "palato-alveolar", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʒ": { + "place": "palato-alveolar", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʂ": { + "place": "retroflex", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʐ": { + "place": "retroflex", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "ç": { + "place": "palatal", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʝ": { + "place": "palatal", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "x": { + "place": "velar", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɣ": { + "place": "velar", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "χ": { + "place": "uvular", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʁ": { + "place": "uvular", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ħ": { + "place": "pharyngeal", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ʕ": { + "place": "pharyngeal", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "h": { + "place": "glottal", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɦ": { + "place": "glottal", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɬ": { + "place": "alveolar", + "manner": "fricative", + "syllabic": "minus", + "voice": "minus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "plus", + "aspirated": "minus", + }, + "ɮ": { + "place": "alveolar", + "manner": "fricative", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "plus", + "aspirated": "minus", + }, + "ʋ": { + "place": "labiodental", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɹ": { + "place": "alveolar", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɻ": { + "place": "retroflex", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "plus", + "lateral": "minus", + "aspirated": "minus", + }, + "j": { + "place": "palatal", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "ɰ": { + "place": "velar", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + "l": { + "place": "alveolar", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "plus", + "aspirated": "minus", + }, + "w": { + "place": "labiovelar", + "manner": "approximant", + "syllabic": "minus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "aspirated": "minus", + }, + # Vowels + "i": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "front", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "y": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "front", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "e": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "E": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "minus", + "long": "plus", + "aspirated": "minus", + }, + "ø": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ø̞": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ɛ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "œ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "æ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "front", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "a": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "front", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ä": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "central", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɐ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "central", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɶ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "front", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "A": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "front", + "round": "minus", + "long": "plus", + "aspirated": "minus", + }, + "ɨ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "central", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ʉ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "central", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ə": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "central", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɜ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "central", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɞ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "central", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "u": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "back", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "U": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "back", + "round": "plus", + "long": "plus", + "aspirated": "minus", + }, + "o": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "o̞": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "O": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "plus", + "long": "plus", + "aspirated": "minus", + }, + "ɔ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ʌ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɒ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "back", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ɑ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "low", + "back": "back", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "I": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "front", + "round": "minus", + "long": "plus", + "aspirated": "minus", + }, + "ɯ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "back", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ʏ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "front", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ʊ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "high", + "back": "back", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ɘ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "central", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "e̞": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "front", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɵ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "central", + "round": "plus", + "long": "minus", + "aspirated": "minus", + }, + "ɤ": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, + "ɤ̞": { + "place": "vowel", + "manner": "vowel2", + "syllabic": "plus", + "voice": "plus", + "nasal": "minus", + "retroflex": "minus", + "lateral": "minus", + "high": "mid", + "back": "back", + "round": "minus", + "long": "minus", + "aspirated": "minus", + }, +} + +# === Algorithm === + + +def align(str1, str2, epsilon=0): + """ + Compute the alignment of two phonetic strings. + + :param str str1: First string to be aligned + :param str str2: Second string to be aligned + + :type epsilon: float (0.0 to 1.0) + :param epsilon: Adjusts threshold similarity score for near-optimal alignments + + :rtype: list(list(tuple(str, str))) + :return: Alignment(s) of str1 and str2 + + (Kondrak 2002: 51) + """ + if np is None: + raise ImportError("You need numpy in order to use the align function") + + assert 0.0 <= epsilon <= 1.0, "Epsilon must be between 0.0 and 1.0." + + # Validate that all input segments exist in the feature matrix + _validate_segments(str1, "str1") + _validate_segments(str2, "str2") + + m = len(str1) + n = len(str2) + # This includes Kondrak's initialization of row 0 and column 0 to all 0s. + S = np.zeros((m + 1, n + 1), dtype=float) + + # If i <= 1 or j <= 1, don't allow expansions as it doesn't make sense, + # and breaks array and string indices. Make sure they never get chosen + # by setting them to -inf. + for i in range(1, m + 1): + for j in range(1, n + 1): + edit1 = S[i - 1, j] + sigma_skip(str1[i - 1]) + edit2 = S[i, j - 1] + sigma_skip(str2[j - 1]) + edit3 = S[i - 1, j - 1] + sigma_sub(str1[i - 1], str2[j - 1]) + if i > 1: + edit4 = S[i - 2, j - 1] + sigma_exp(str2[j - 1], str1[i - 2 : i]) + else: + edit4 = -inf + if j > 1: + edit5 = S[i - 1, j - 2] + sigma_exp(str1[i - 1], str2[j - 2 : j]) + else: + edit5 = -inf + S[i, j] = max(edit1, edit2, edit3, edit4, edit5, 0) + + T = (1 - epsilon) * np.amax(S) # Threshold score for near-optimal alignments + + alignments = [] + for i in range(1, m + 1): + for j in range(1, n + 1): + if S[i, j] >= T: + alignments.append(_retrieve(i, j, 0, S, T, str1, str2, [])) + return alignments + + +def _retrieve(i, j, s, S, T, str1, str2, out): + """ + Retrieve the path through the similarity matrix S starting at (i, j). + + :rtype: list(tuple(str, str)) + :return: Alignment of str1 and str2 + """ + if S[i, j] == 0: + return out + else: + if j > 1 and S[i - 1, j - 2] + sigma_exp(str1[i - 1], str2[j - 2 : j]) + s >= T: + out.insert(0, (str1[i - 1], str2[j - 2 : j])) + _retrieve( + i - 1, + j - 2, + s + sigma_exp(str1[i - 1], str2[j - 2 : j]), + S, + T, + str1, + str2, + out, + ) + elif ( + i > 1 and S[i - 2, j - 1] + sigma_exp(str2[j - 1], str1[i - 2 : i]) + s >= T + ): + out.insert(0, (str1[i - 2 : i], str2[j - 1])) + _retrieve( + i - 2, + j - 1, + s + sigma_exp(str2[j - 1], str1[i - 2 : i]), + S, + T, + str1, + str2, + out, + ) + elif S[i, j - 1] + sigma_skip(str2[j - 1]) + s >= T: + out.insert(0, ("-", str2[j - 1])) + _retrieve(i, j - 1, s + sigma_skip(str2[j - 1]), S, T, str1, str2, out) + elif S[i - 1, j] + sigma_skip(str1[i - 1]) + s >= T: + out.insert(0, (str1[i - 1], "-")) + _retrieve(i - 1, j, s + sigma_skip(str1[i - 1]), S, T, str1, str2, out) + elif S[i - 1, j - 1] + sigma_sub(str1[i - 1], str2[j - 1]) + s >= T: + out.insert(0, (str1[i - 1], str2[j - 1])) + _retrieve( + i - 1, + j - 1, + s + sigma_sub(str1[i - 1], str2[j - 1]), + S, + T, + str1, + str2, + out, + ) + return out + + +def _validate_segments(seq, name): + """Validate that all segments in seq exist in the feature matrix.""" + for i, char in enumerate(seq): + if char not in feature_matrix: + raise ValueError( + f"Segment '{char}' at position {i} in {name} not found in feature_matrix" + ) + + +def sigma_skip(p): + """ + Returns score of an indel of P. + + (Kondrak 2002: 54) + """ + return C_skip + + +def sigma_sub(p, q): + """ + Returns score of a substitution of P with Q. + + (Kondrak 2002: 54) + """ + return C_sub - delta(p, q) - V(p) - V(q) + + +def sigma_exp(p, q): + """ + Returns score of an expansion/compression. + + (Kondrak 2002: 54) + """ + if len(q) != 2: + raise ValueError(f"sigma_exp expects q of length 2, got {len(q)}") + q1 = q[0] + q2 = q[1] + return C_exp - delta(p, q1) - delta(p, q2) - V(p) - max(V(q1), V(q2)) + + +def delta(p, q): + """ + Return weighted sum of difference between P and Q. + + (Kondrak 2002: 54) + """ + features = R(p, q) + total = 0 + if np is not None: + return np.dot( + [diff(p, q, f) for f in features], [salience[f] for f in features] + ) + for f in features: + total += diff(p, q, f) * salience[f] + return total + + +def diff(p, q, f): + """ + Returns difference between phonetic segments P and Q for feature F. + + (Kondrak 2002: 52, 54) + """ + p_features, q_features = feature_matrix[p], feature_matrix[q] + return abs(similarity_matrix[p_features[f]] - similarity_matrix[q_features[f]]) + + +def R(p, q): + """ + Return relevant features for segment comparison. + + (Kondrak 2002: 54) + """ + if p in consonants or q in consonants: + return R_c + return R_v + + +def V(p): + """ + Return vowel weight if P is vowel. + + (Kondrak 2002: 54) + """ + if p in consonants: + return 0 + return C_vwl + + +# === Test === + + +def demo(): + """ + A demonstration of the result of aligning phonetic sequences + used in Kondrak's (2002) dissertation. + """ + data = [pair.split(",") for pair in cognate_data.split("\n")] + for pair in data: + alignment = align(pair[0], pair[1])[0] + alignment = [f"({a[0]}, {a[1]})" for a in alignment] + alignment = " ".join(alignment) + print(f"{pair[0]} ~ {pair[1]} : {alignment}") + + +cognate_data = """jo,ʒə +tu,ty +nosotros,nu +kjen,ki +ke,kwa +todos,tu +una,ən +dos,dø +tres,trwa +ombre,om +arbol,arbrə +pluma,plym +kabeθa,kap +boka,buʃ +pje,pje +koraθon,kœr +ber,vwar +benir,vənir +deθir,dir +pobre,povrə +ðis,dIzes +ðæt,das +wat,vas +nat,nixt +loŋ,laŋ +mæn,man +fleʃ,flajʃ +bləd,blyt +feðər,fEdər +hær,hAr +ir,Or +aj,awgə +nowz,nAzə +mawθ,munt +təŋ,tsuŋə +fut,fys +nij,knI +hænd,hant +hart,herts +livər,lEbər +ænd,ante +æt,ad +blow,flAre +ir,awris +ijt,edere +fiʃ,piʃkis +flow,fluere +staɾ,stella +ful,plenus +græs,gramen +hart,kordis +horn,korny +aj,ego +nij,genU +məðər,mAter +mawntən,mons +nejm,nomen +njuw,nowus +wən,unus +rawnd,rotundus +sow,suere +sit,sedere +θrij,tres +tuwθ,dentis +θin,tenwis +kinwawa,kenuaʔ +nina,nenah +napewa,napɛw +wapimini,wapemen +namesa,namɛʔs +okimawa,okemaw +ʃiʃipa,seʔsep +ahkohkwa,ahkɛh +pematesiweni,pematesewen +asenja,aʔsɛn""" + + +if __name__ == "__main__": + demo() diff --git a/nltk/metrics/artstein_poesio_example.txt b/nltk/metrics/artstein_poesio_example.txt new file mode 100644 index 0000000..cad1b46 --- /dev/null +++ b/nltk/metrics/artstein_poesio_example.txt @@ -0,0 +1,200 @@ +a 1 stat +b 1 stat +a 2 stat +b 2 stat +a 3 stat +b 3 stat +a 4 stat +b 4 stat +a 5 stat +b 5 stat +a 6 stat +b 6 stat +a 7 stat +b 7 stat +a 8 stat +b 8 stat +a 9 stat +b 9 stat +a 10 stat +b 10 stat +a 11 stat +b 11 stat +a 12 stat +b 12 stat +a 13 stat +b 13 stat +a 14 stat +b 14 stat +a 15 stat +b 15 stat +a 16 stat +b 16 stat +a 17 stat +b 17 stat +a 18 stat +b 18 stat +a 19 stat +b 19 stat +a 20 stat +b 20 stat +a 21 stat +b 21 stat +a 22 stat +b 22 stat +a 23 stat +b 23 stat +a 24 stat +b 24 stat +a 25 stat +b 25 stat +a 26 stat +b 26 stat +a 27 stat +b 27 stat +a 28 stat +b 28 stat +a 29 stat +b 29 stat +a 30 stat +b 30 stat +a 31 stat +b 31 stat +a 32 stat +b 32 stat +a 33 stat +b 33 stat +a 34 stat +b 34 stat +a 35 stat +b 35 stat +a 36 stat +b 36 stat +a 37 stat +b 37 stat +a 38 stat +b 38 stat +a 39 stat +b 39 stat +a 40 stat +b 40 stat +a 41 stat +b 41 stat +a 42 stat +b 42 stat +a 43 stat +b 43 stat +a 44 stat +b 44 stat +a 45 stat +b 45 stat +a 46 stat +b 46 stat +a 47 ireq +b 47 stat +a 48 ireq +b 48 stat +a 49 ireq +b 49 stat +a 50 ireq +b 50 stat +a 51 ireq +b 51 stat +a 52 ireq +b 52 stat +a 53 ireq +b 53 ireq +a 54 ireq +b 54 ireq +a 55 ireq +b 55 ireq +a 56 ireq +b 56 ireq +a 57 ireq +b 57 ireq +a 58 ireq +b 58 ireq +a 59 ireq +b 59 ireq +a 60 ireq +b 60 ireq +a 61 ireq +b 61 ireq +a 62 ireq +b 62 ireq +a 63 ireq +b 63 ireq +a 64 ireq +b 64 ireq +a 65 ireq +b 65 ireq +a 66 ireq +b 66 ireq +a 67 ireq +b 67 ireq +a 68 ireq +b 68 ireq +a 69 ireq +b 69 ireq +a 70 ireq +b 70 ireq +a 71 ireq +b 71 ireq +a 72 ireq +b 72 ireq +a 73 ireq +b 73 ireq +a 74 ireq +b 74 ireq +a 75 ireq +b 75 ireq +a 76 ireq +b 76 ireq +a 77 ireq +b 77 ireq +a 78 ireq +b 78 ireq +a 79 ireq +b 79 ireq +a 80 ireq +b 80 ireq +a 81 ireq +b 81 ireq +a 82 ireq +b 82 ireq +a 83 ireq +b 83 ireq +a 84 ireq +b 84 ireq +a 85 ireq +b 85 chck +a 86 ireq +b 86 chck +a 87 ireq +b 87 chck +a 88 ireq +b 88 chck +a 89 ireq +b 89 chck +a 90 ireq +b 90 chck +a 91 chck +b 91 chck +a 92 chck +b 92 chck +a 93 chck +b 93 chck +a 94 chck +b 94 chck +a 95 chck +b 95 chck +a 96 chck +b 96 chck +a 97 chck +b 97 chck +a 98 chck +b 98 chck +a 99 chck +b 99 chck +a 100 chck +b 100 chck diff --git a/nltk/metrics/association.py b/nltk/metrics/association.py new file mode 100644 index 0000000..6a108bd --- /dev/null +++ b/nltk/metrics/association.py @@ -0,0 +1,476 @@ +# Natural Language Toolkit: Ngram Association Measures +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Joel Nothman +# URL: +# For license information, see LICENSE.TXT + +""" +Provides scoring functions for a number of association measures through a +generic, abstract implementation in ``NgramAssocMeasures``, and n-specific +``BigramAssocMeasures`` and ``TrigramAssocMeasures``. +""" + +import math as _math +from abc import ABCMeta, abstractmethod +from functools import reduce + +_log2 = lambda x: _math.log2(x) +_ln = _math.log + +_product = lambda s: reduce(lambda x, y: x * y, s) + +_SMALL = 1e-20 + +try: + from scipy.stats import fisher_exact +except ImportError: + + def fisher_exact(*_args, **_kwargs): + raise NotImplementedError + + +### Indices to marginals arguments: + +NGRAM = 0 +"""Marginals index for the ngram count""" + +UNIGRAMS = -2 +"""Marginals index for a tuple of each unigram count""" + +TOTAL = -1 +"""Marginals index for the number of words in the data""" + + +class NgramAssocMeasures(metaclass=ABCMeta): + """ + An abstract class defining a collection of generic association measures. + Each public method returns a score, taking the following arguments:: + + score_fn(count_of_ngram, + (count_of_n-1gram_1, ..., count_of_n-1gram_j), + (count_of_n-2gram_1, ..., count_of_n-2gram_k), + ..., + (count_of_1gram_1, ..., count_of_1gram_n), + count_of_total_words) + + See ``BigramAssocMeasures`` and ``TrigramAssocMeasures`` + + Inheriting classes should define a property _n, and a method _contingency + which calculates contingency values from marginals in order for all + association measures defined here to be usable. + """ + + _n = 0 + + @staticmethod + @abstractmethod + def _contingency(*marginals): + """Calculates values of a contingency table from marginal values.""" + raise NotImplementedError( + "The contingency table is not available" "in the general ngram case" + ) + + @staticmethod + @abstractmethod + def _marginals(*contingency): + """Calculates values of contingency table marginals from its values.""" + raise NotImplementedError( + "The contingency table is not available" "in the general ngram case" + ) + + @classmethod + def _expected_values(cls, cont): + """Calculates expected values for a contingency table.""" + n_all = sum(cont) + bits = [1 << i for i in range(cls._n)] + + # For each contingency table cell + for i in range(len(cont)): + # Yield the expected value + yield ( + _product( + sum(cont[x] for x in range(2**cls._n) if (x & j) == (i & j)) + for j in bits + ) + / (n_all ** (cls._n - 1)) + ) + + @staticmethod + def raw_freq(*marginals): + """Scores ngrams by their frequency""" + return marginals[NGRAM] / marginals[TOTAL] + + @classmethod + def student_t(cls, *marginals): + """Scores ngrams using Student's t test with independence hypothesis + for unigrams, as in Manning and Schutze 5.3.1. + """ + return ( + marginals[NGRAM] + - _product(marginals[UNIGRAMS]) / (marginals[TOTAL] ** (cls._n - 1)) + ) / (marginals[NGRAM] + _SMALL) ** 0.5 + + @classmethod + def chi_sq(cls, *marginals): + """Scores ngrams using Pearson's chi-square as in Manning and Schutze + 5.3.3. + """ + cont = cls._contingency(*marginals) + exps = cls._expected_values(cont) + return sum((obs - exp) ** 2 / (exp + _SMALL) for obs, exp in zip(cont, exps)) + + @staticmethod + def mi_like(*marginals, **kwargs): + """Scores ngrams using a variant of mutual information. The keyword + argument power sets an exponent (default 3) for the numerator. No + logarithm of the result is calculated. + """ + return marginals[NGRAM] ** kwargs.get("power", 3) / _product( + marginals[UNIGRAMS] + ) + + @classmethod + def pmi(cls, *marginals): + """Scores ngrams by pointwise mutual information, as in Manning and + Schutze 5.4. + """ + return _log2(marginals[NGRAM] * marginals[TOTAL] ** (cls._n - 1)) - _log2( + _product(marginals[UNIGRAMS]) + ) + + @classmethod + def likelihood_ratio(cls, *marginals): + """Scores ngrams using likelihood ratios as in Manning and Schutze 5.3.4.""" + cont = cls._contingency(*marginals) + return 2 * sum( + obs * _ln(obs / (exp + _SMALL) + _SMALL) + for obs, exp in zip(cont, cls._expected_values(cont)) + ) + + @classmethod + def poisson_stirling(cls, *marginals): + """Scores ngrams using the Poisson-Stirling measure.""" + exp = _product(marginals[UNIGRAMS]) / (marginals[TOTAL] ** (cls._n - 1)) + return marginals[NGRAM] * (_log2(marginals[NGRAM] / exp) - 1) + + @classmethod + def jaccard(cls, *marginals): + """Scores ngrams using the Jaccard index.""" + cont = cls._contingency(*marginals) + return cont[0] / sum(cont[:-1]) + + +class BigramAssocMeasures(NgramAssocMeasures): + """ + A collection of bigram association measures. Each association measure + is provided as a function with three arguments:: + + bigram_score_fn(n_ii, (n_ix, n_xi), n_xx) + + The arguments constitute the marginals of a contingency table, counting + the occurrences of particular events in a corpus. The letter i in the + suffix refers to the appearance of the word in question, while x indicates + the appearance of any word. Thus, for example: + + - n_ii counts ``(w1, w2)``, i.e. the bigram being scored + - n_ix counts ``(w1, *)`` + - n_xi counts ``(*, w2)`` + - n_xx counts ``(*, *)``, i.e. any bigram + + This may be shown with respect to a contingency table:: + + w1 ~w1 + ------ ------ + w2 | n_ii | n_oi | = n_xi + ------ ------ + ~w2 | n_io | n_oo | + ------ ------ + = n_ix TOTAL = n_xx + """ + + _n = 2 + + @staticmethod + def _contingency(n_ii, n_ix_xi_tuple, n_xx): + """Calculates values of a bigram contingency table from marginal values.""" + (n_ix, n_xi) = n_ix_xi_tuple + n_oi = n_xi - n_ii + n_io = n_ix - n_ii + return (n_ii, n_oi, n_io, n_xx - n_ii - n_oi - n_io) + + @staticmethod + def _marginals(n_ii, n_oi, n_io, n_oo): + """Calculates values of contingency table marginals from its values.""" + return (n_ii, (n_oi + n_ii, n_io + n_ii), n_oo + n_oi + n_io + n_ii) + + @staticmethod + def _expected_values(cont): + """Calculates expected values for a contingency table.""" + n_xx = sum(cont) + # For each contingency table cell + for i in range(4): + yield (cont[i] + cont[i ^ 1]) * (cont[i] + cont[i ^ 2]) / n_xx + + @classmethod + def phi_sq(cls, *marginals): + """Scores bigrams using phi-square, the square of the Pearson correlation + coefficient. + """ + n_ii, n_io, n_oi, n_oo = cls._contingency(*marginals) + + return (n_ii * n_oo - n_io * n_oi) ** 2 / ( + (n_ii + n_io) * (n_ii + n_oi) * (n_io + n_oo) * (n_oi + n_oo) + ) + + @classmethod + def chi_sq(cls, n_ii, n_ix_xi_tuple, n_xx): + """Scores bigrams using chi-square, i.e. phi-sq multiplied by the number + of bigrams, as in Manning and Schutze 5.3.3. + """ + (n_ix, n_xi) = n_ix_xi_tuple + return n_xx * cls.phi_sq(n_ii, (n_ix, n_xi), n_xx) + + @classmethod + def fisher(cls, *marginals): + """Scores bigrams using Fisher's Exact Test (Pedersen 1996). Less + sensitive to small counts than PMI or Chi Sq, but also more expensive + to compute. Requires scipy. + """ + + n_ii, n_io, n_oi, n_oo = cls._contingency(*marginals) + + (odds, pvalue) = fisher_exact([[n_ii, n_io], [n_oi, n_oo]], alternative="less") + return pvalue + + @staticmethod + def dice(n_ii, n_ix_xi_tuple, n_xx): + """Scores bigrams using Dice's coefficient.""" + (n_ix, n_xi) = n_ix_xi_tuple + return 2 * n_ii / (n_ix + n_xi) + + +class TrigramAssocMeasures(NgramAssocMeasures): + """ + A collection of trigram association measures. Each association measure + is provided as a function with four arguments:: + + trigram_score_fn(n_iii, + (n_iix, n_ixi, n_xii), + (n_ixx, n_xix, n_xxi), + n_xxx) + + The arguments constitute the marginals of a contingency table, counting + the occurrences of particular events in a corpus. The letter i in the + suffix refers to the appearance of the word in question, while x indicates + the appearance of any word. Thus, for example: + + - n_iii counts ``(w1, w2, w3)``, i.e. the trigram being scored + - n_ixx counts ``(w1, *, *)`` + - n_xxx counts ``(*, *, *)``, i.e. any trigram + """ + + _n = 3 + + @staticmethod + def _contingency(n_iii, n_iix_tuple, n_ixx_tuple, n_xxx): + """Calculates values of a trigram contingency table (or cube) from + marginal values. + >>> TrigramAssocMeasures._contingency(1, (1, 1, 1), (1, 73, 1), 2000) + (1, 0, 0, 0, 0, 72, 0, 1927) + """ + (n_iix, n_ixi, n_xii) = n_iix_tuple + (n_ixx, n_xix, n_xxi) = n_ixx_tuple + n_oii = n_xii - n_iii + n_ioi = n_ixi - n_iii + n_iio = n_iix - n_iii + n_ooi = n_xxi - n_iii - n_oii - n_ioi + n_oio = n_xix - n_iii - n_oii - n_iio + n_ioo = n_ixx - n_iii - n_ioi - n_iio + n_ooo = n_xxx - n_iii - n_oii - n_ioi - n_iio - n_ooi - n_oio - n_ioo + + return (n_iii, n_oii, n_ioi, n_ooi, n_iio, n_oio, n_ioo, n_ooo) + + @staticmethod + def _marginals(*contingency): + """Calculates values of contingency table marginals from its values. + >>> TrigramAssocMeasures._marginals(1, 0, 0, 0, 0, 72, 0, 1927) + (1, (1, 1, 1), (1, 73, 1), 2000) + """ + n_iii, n_oii, n_ioi, n_ooi, n_iio, n_oio, n_ioo, n_ooo = contingency + return ( + n_iii, + (n_iii + n_iio, n_iii + n_ioi, n_iii + n_oii), + ( + n_iii + n_ioi + n_iio + n_ioo, + n_iii + n_oii + n_iio + n_oio, + n_iii + n_oii + n_ioi + n_ooi, + ), + sum(contingency), + ) + + +class QuadgramAssocMeasures(NgramAssocMeasures): + """ + A collection of quadgram association measures. Each association measure + is provided as a function with five arguments:: + + trigram_score_fn(n_iiii, + (n_iiix, n_iixi, n_ixii, n_xiii), + (n_iixx, n_ixix, n_ixxi, n_xixi, n_xxii, n_xiix), + (n_ixxx, n_xixx, n_xxix, n_xxxi), + n_all) + + The arguments constitute the marginals of a contingency table, counting + the occurrences of particular events in a corpus. The letter i in the + suffix refers to the appearance of the word in question, while x indicates + the appearance of any word. Thus, for example: + + - n_iiii counts ``(w1, w2, w3, w4)``, i.e. the quadgram being scored + - n_ixxi counts ``(w1, *, *, w4)`` + - n_xxxx counts ``(*, *, *, *)``, i.e. any quadgram + """ + + _n = 4 + + @staticmethod + def _contingency(n_iiii, n_iiix_tuple, n_iixx_tuple, n_ixxx_tuple, n_xxxx): + """Calculates values of a quadgram contingency table from + marginal values. + """ + (n_iiix, n_iixi, n_ixii, n_xiii) = n_iiix_tuple + (n_iixx, n_ixix, n_ixxi, n_xixi, n_xxii, n_xiix) = n_iixx_tuple + (n_ixxx, n_xixx, n_xxix, n_xxxi) = n_ixxx_tuple + n_oiii = n_xiii - n_iiii + n_ioii = n_ixii - n_iiii + n_iioi = n_iixi - n_iiii + n_ooii = n_xxii - n_iiii - n_oiii - n_ioii + n_oioi = n_xixi - n_iiii - n_oiii - n_iioi + n_iooi = n_ixxi - n_iiii - n_ioii - n_iioi + n_oooi = n_xxxi - n_iiii - n_oiii - n_ioii - n_iioi - n_ooii - n_iooi - n_oioi + n_iiio = n_iiix - n_iiii + n_oiio = n_xiix - n_iiii - n_oiii - n_iiio + n_ioio = n_ixix - n_iiii - n_ioii - n_iiio + n_ooio = n_xxix - n_iiii - n_oiii - n_ioii - n_iiio - n_ooii - n_ioio - n_oiio + n_iioo = n_iixx - n_iiii - n_iioi - n_iiio + n_oioo = n_xixx - n_iiii - n_oiii - n_iioi - n_iiio - n_oioi - n_oiio - n_iioo + n_iooo = n_ixxx - n_iiii - n_ioii - n_iioi - n_iiio - n_iooi - n_iioo - n_ioio + n_oooo = ( + n_xxxx + - n_iiii + - n_oiii + - n_ioii + - n_iioi + - n_ooii + - n_oioi + - n_iooi + - n_oooi + - n_iiio + - n_oiio + - n_ioio + - n_ooio + - n_iioo + - n_oioo + - n_iooo + ) + + return ( + n_iiii, + n_oiii, + n_ioii, + n_ooii, + n_iioi, + n_oioi, + n_iooi, + n_oooi, + n_iiio, + n_oiio, + n_ioio, + n_ooio, + n_iioo, + n_oioo, + n_iooo, + n_oooo, + ) + + @staticmethod + def _marginals(*contingency): + """Calculates values of contingency table marginals from its values. + QuadgramAssocMeasures._marginals(1, 0, 2, 46, 552, 825, 2577, 34967, 1, 0, 2, 48, 7250, 9031, 28585, 356653) + (1, (2, 553, 3, 1), (7804, 6, 3132, 1378, 49, 2), (38970, 17660, 100, 38970), 440540) + """ + ( + n_iiii, + n_oiii, + n_ioii, + n_ooii, + n_iioi, + n_oioi, + n_iooi, + n_oooi, + n_iiio, + n_oiio, + n_ioio, + n_ooio, + n_iioo, + n_oioo, + n_iooo, + n_oooo, + ) = contingency + + n_iiix = n_iiii + n_iiio + n_iixi = n_iiii + n_iioi + n_ixii = n_iiii + n_ioii + n_xiii = n_iiii + n_oiii + + n_iixx = n_iiii + n_iioi + n_iiio + n_iioo + n_ixix = n_iiii + n_ioii + n_iiio + n_ioio + n_ixxi = n_iiii + n_ioii + n_iioi + n_iooi + n_xixi = n_iiii + n_oiii + n_iioi + n_oioi + n_xxii = n_iiii + n_oiii + n_ioii + n_ooii + n_xiix = n_iiii + n_oiii + n_iiio + n_oiio + + n_ixxx = n_iiii + n_ioii + n_iioi + n_iiio + n_iooi + n_iioo + n_ioio + n_iooo + n_xixx = n_iiii + n_oiii + n_iioi + n_iiio + n_oioi + n_oiio + n_iioo + n_oioo + n_xxix = n_iiii + n_oiii + n_ioii + n_iiio + n_ooii + n_ioio + n_oiio + n_ooio + n_xxxi = n_iiii + n_oiii + n_ioii + n_iioi + n_ooii + n_iooi + n_oioi + n_oooi + + n_all = sum(contingency) + + return ( + n_iiii, + (n_iiix, n_iixi, n_ixii, n_xiii), + (n_iixx, n_ixix, n_ixxi, n_xixi, n_xxii, n_xiix), + (n_ixxx, n_xixx, n_xxix, n_xxxi), + n_all, + ) + + +class ContingencyMeasures: + """Wraps NgramAssocMeasures classes such that the arguments of association + measures are contingency table values rather than marginals. + """ + + def __init__(self, measures): + """Constructs a ContingencyMeasures given a NgramAssocMeasures class""" + self.__class__.__name__ = "Contingency" + measures.__class__.__name__ + for k in dir(measures): + if k.startswith("__"): + continue + v = getattr(measures, k) + if not k.startswith("_"): + v = self._make_contingency_fn(measures, v) + setattr(self, k, v) + + @staticmethod + def _make_contingency_fn(measures, old_fn): + """From an association measure function, produces a new function which + accepts contingency table values as its arguments. + """ + + def res(*contingency): + return old_fn(*measures._marginals(*contingency)) + + res.__doc__ = old_fn.__doc__ + res.__name__ = old_fn.__name__ + return res diff --git a/nltk/metrics/confusionmatrix.py b/nltk/metrics/confusionmatrix.py new file mode 100644 index 0000000..9ada779 --- /dev/null +++ b/nltk/metrics/confusionmatrix.py @@ -0,0 +1,380 @@ +# Natural Language Toolkit: Confusion Matrices +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# Tom Aarsen <> +# URL: +# For license information, see LICENSE.TXT + +from nltk.probability import FreqDist + + +class ConfusionMatrix: + """ + The confusion matrix between a list of reference values and a + corresponding list of test values. Entry *[r,t]* of this + matrix is a count of the number of times that the reference value + *r* corresponds to the test value *t*. E.g.: + + >>> from nltk.metrics import ConfusionMatrix + >>> ref = 'DET NN VB DET JJ NN NN IN DET NN'.split() + >>> test = 'DET VB VB DET NN NN NN IN DET NN'.split() + >>> cm = ConfusionMatrix(ref, test) + >>> print(cm['NN', 'NN']) + 3 + + Note that the diagonal entries *Ri=Tj* of this matrix + corresponds to correct values; and the off-diagonal entries + correspond to incorrect values. + """ + + def __init__(self, reference, test, sort_by_count=False): + """ + Construct a new confusion matrix from a list of reference + values and a corresponding list of test values. + + :type reference: list + :param reference: An ordered list of reference values. + :type test: list + :param test: A list of values to compare against the + corresponding reference values. + :raise ValueError: If ``reference`` and ``length`` do not have + the same length. + """ + if len(reference) != len(test): + raise ValueError("Lists must have the same length.") + + # Get a list of all values. + if sort_by_count: + ref_fdist = FreqDist(reference) + test_fdist = FreqDist(test) + + def key(v): + return -(ref_fdist[v] + test_fdist[v]) + + values = sorted(set(reference + test), key=key) + else: + values = sorted(set(reference + test)) + + # Construct a value->index dictionary + indices = {val: i for (i, val) in enumerate(values)} + + # Make a sparse confusion matrix: a dict mapping each observed + # (reference index, test index) pair to its count. A dense V x V table + # would allocate V**2 cells even when only a few pairs occur, so an + # all-distinct input (V == number of items) costs O(V**2) memory and + # OOM-kills the worker (CWE-770; CVE-2026-12839). The sparse map costs + # only as much as the observed pairs. + # Row totals (count per reference index) are accumulated here in the + # same single pass. ``sort_by_count`` orders labels by their row total, + # so caching them keeps that lookup O(1); recomputing a total by + # scanning the sparse map on each call would make the sort O(V * nnz), + # i.e. quadratic again for a large all-distinct input. + confusion = {} + row_totals = {} + max_conf = 0 # Maximum confusion + for w, g in zip(reference, test): + i = indices[w] + pair = (i, indices[g]) + count = confusion.get(pair, 0) + 1 + confusion[pair] = count + row_totals[i] = row_totals.get(i, 0) + 1 + if count > max_conf: + max_conf = count + + #: A list of all values in ``reference`` or ``test``. + self._values = values + #: A dictionary mapping values in ``self._values`` to their indices. + self._indices = indices + #: The confusion matrix itself, as a sparse dict mapping each observed + #: ``(reference index, test index)`` pair to its count. + self._confusion = confusion + #: Cached per-reference-row totals, keyed by reference index, so + #: ``sort_by_count`` lookups are O(1) (see ``_row_total``). + self._row_totals = row_totals + #: The greatest count in ``self._confusion`` (used for printing). + self._max_conf = max_conf + #: The total number of values in the confusion matrix. + self._total = len(reference) + #: The number of correct (on-diagonal) values in the matrix. + self._correct = sum(c for (i, j), c in confusion.items() if i == j) + + def _row_total(self, i): + """Total count in row ``i`` (alignments from reference value ``i``). + + Read from the cache built once in ``__init__`` (a single O(nnz) pass), + so ``sort_by_count`` can order all labels in O(V log V) rather than + rescanning the sparse map per label (O(V * nnz), quadratic for a large + all-distinct input). + """ + return self._row_totals.get(i, 0) + + def __getitem__(self, li_lj_tuple): + """ + :return: The number of times that value ``li`` was expected and + value ``lj`` was given. + :rtype: int + """ + (li, lj) = li_lj_tuple + i = self._indices[li] + j = self._indices[lj] + return self._confusion.get((i, j), 0) + + def __repr__(self): + return f"" + + def __str__(self): + return self.pretty_format() + + def pretty_format( + self, + show_percents=False, + values_in_chart=True, + truncate=None, + sort_by_count=False, + ): + """ + :return: A multi-line string representation of this confusion matrix. + :type truncate: int + :param truncate: If specified, then only show the specified + number of values. Any sorting (e.g., sort_by_count) + will be performed before truncation. + :param sort_by_count: If true, then sort by the count of each + label in the reference data. I.e., labels that occur more + frequently in the reference label will be towards the left + edge of the matrix, and labels that occur less frequently + will be towards the right edge. + + @todo: add marginals? + """ + confusion = self._confusion + + values = self._values + if sort_by_count: + values = sorted(values, key=lambda v: -self._row_total(self._indices[v])) + + if truncate: + values = values[:truncate] + + if values_in_chart: + value_strings = ["%s" % val for val in values] + else: + value_strings = [str(n + 1) for n in range(len(values))] + + # Construct a format string for row values + valuelen = max(len(val) for val in value_strings) + value_format = "%" + repr(valuelen) + "s | " + # Construct a format string for matrix entries + if show_percents: + entrylen = 6 + entry_format = "%5.1f%%" + zerostr = " ." + else: + entrylen = len(repr(self._max_conf)) + entry_format = "%" + repr(entrylen) + "d" + zerostr = " " * (entrylen - 1) + "." + + # Write the column values. + s = "" + for i in range(valuelen): + s += (" " * valuelen) + " |" + for val in value_strings: + if i >= valuelen - len(val): + s += val[i - valuelen + len(val)].rjust(entrylen + 1) + else: + s += " " * (entrylen + 1) + s += " |\n" + + # Write a dividing line + s += "{}-+-{}+\n".format("-" * valuelen, "-" * ((entrylen + 1) * len(values))) + + # Write the entries. + for val, li in zip(value_strings, values): + i = self._indices[li] + s += value_format % val + for lj in values: + j = self._indices[lj] + count = confusion.get((i, j), 0) + if count == 0: + s += zerostr + elif show_percents: + s += entry_format % (100.0 * count / self._total) + else: + s += entry_format % count + if i == j: + prevspace = s.rfind(" ") + s = s[:prevspace] + "<" + s[prevspace + 1 :] + ">" + else: + s += " " + s += "|\n" + + # Write a dividing line + s += "{}-+-{}+\n".format("-" * valuelen, "-" * ((entrylen + 1) * len(values))) + + # Write a key + s += "(row = reference; col = test)\n" + if not values_in_chart: + s += "Value key:\n" + for i, value in enumerate(values): + s += "%6d: %s\n" % (i + 1, value) + + return s + + def key(self): + values = self._values + str = "Value key:\n" + indexlen = len(repr(len(values) - 1)) + key_format = " %" + repr(indexlen) + "d: %s\n" + str += "".join([key_format % (i, values[i]) for i in range(len(values))]) + return str + + def recall(self, value): + """Given a value in the confusion matrix, return the recall + that corresponds to this value. The recall is defined as: + + - *r* = true positive / (true positive + false positive) + + and can loosely be considered the ratio of how often ``value`` + was predicted correctly relative to how often ``value`` was + the true result. + + :param value: value used in the ConfusionMatrix + :return: the recall corresponding to ``value``. + :rtype: float + """ + # Number of times `value` was correct, and also predicted + TP = self[value, value] + # Number of times `value` was correct + TP_FN = sum(self[value, pred_value] for pred_value in self._values) + if TP_FN == 0: + return 0.0 + return TP / TP_FN + + def precision(self, value): + """Given a value in the confusion matrix, return the precision + that corresponds to this value. The precision is defined as: + + - *p* = true positive / (true positive + false negative) + + and can loosely be considered the ratio of how often ``value`` + was predicted correctly relative to the number of predictions + for ``value``. + + :param value: value used in the ConfusionMatrix + :return: the precision corresponding to ``value``. + :rtype: float + """ + # Number of times `value` was correct, and also predicted + TP = self[value, value] + # Number of times `value` was predicted + TP_FP = sum(self[real_value, value] for real_value in self._values) + if TP_FP == 0: + return 0.0 + return TP / TP_FP + + def f_measure(self, value, alpha=0.5): + """ + Given a value used in the confusion matrix, return the f-measure + that corresponds to this value. The f-measure is the harmonic mean + of the ``precision`` and ``recall``, weighted by ``alpha``. + In particular, given the precision *p* and recall *r* defined by: + + - *p* = true positive / (true positive + false negative) + - *r* = true positive / (true positive + false positive) + + The f-measure is: + + - *1/(alpha/p + (1-alpha)/r)* + + With ``alpha = 0.5``, this reduces to: + + - *2pr / (p + r)* + + :param value: value used in the ConfusionMatrix + :param alpha: Ratio of the cost of false negative compared to false + positives. Defaults to 0.5, where the costs are equal. + :type alpha: float + :return: the F-measure corresponding to ``value``. + :rtype: float + """ + p = self.precision(value) + r = self.recall(value) + if p == 0.0 or r == 0.0: + return 0.0 + return 1.0 / (alpha / p + (1 - alpha) / r) + + def evaluate(self, alpha=0.5, truncate=None, sort_by_count=False): + """ + Tabulate the **recall**, **precision** and **f-measure** + for each value in this confusion matrix. + + >>> reference = "DET NN VB DET JJ NN NN IN DET NN".split() + >>> test = "DET VB VB DET NN NN NN IN DET NN".split() + >>> cm = ConfusionMatrix(reference, test) + >>> print(cm.evaluate()) + Tag | Prec. | Recall | F-measure + ----+--------+--------+----------- + DET | 1.0000 | 1.0000 | 1.0000 + IN | 1.0000 | 1.0000 | 1.0000 + JJ | 0.0000 | 0.0000 | 0.0000 + NN | 0.7500 | 0.7500 | 0.7500 + VB | 0.5000 | 1.0000 | 0.6667 + + + :param alpha: Ratio of the cost of false negative compared to false + positives, as used in the f-measure computation. Defaults to 0.5, + where the costs are equal. + :type alpha: float + :param truncate: If specified, then only show the specified + number of values. Any sorting (e.g., sort_by_count) + will be performed before truncation. Defaults to None + :type truncate: int, optional + :param sort_by_count: Whether to sort the outputs on frequency + in the reference label. Defaults to False. + :type sort_by_count: bool, optional + :return: A tabulated recall, precision and f-measure string + :rtype: str + """ + tags = self._values + + # Apply keyword parameters + if sort_by_count: + tags = sorted(tags, key=lambda v: -self._row_total(self._indices[v])) + if truncate: + tags = tags[:truncate] + + tag_column_len = max(max(len(tag) for tag in tags), 3) + + # Construct the header + s = ( + f"{' ' * (tag_column_len - 3)}Tag | Prec. | Recall | F-measure\n" + f"{'-' * tag_column_len}-+--------+--------+-----------\n" + ) + + # Construct the body + for tag in tags: + s += ( + f"{tag:>{tag_column_len}} | " + f"{self.precision(tag):<6.4f} | " + f"{self.recall(tag):<6.4f} | " + f"{self.f_measure(tag, alpha=alpha):.4f}\n" + ) + + return s + + +def demo(): + reference = "DET NN VB DET JJ NN NN IN DET NN".split() + test = "DET VB VB DET NN NN NN IN DET NN".split() + print("Reference =", reference) + print("Test =", test) + print("Confusion matrix:") + print(ConfusionMatrix(reference, test)) + print(ConfusionMatrix(reference, test).pretty_format(sort_by_count=True)) + + print(ConfusionMatrix(reference, test).recall("VB")) + + +if __name__ == "__main__": + demo() diff --git a/nltk/metrics/distance.py b/nltk/metrics/distance.py new file mode 100644 index 0000000..d3126d3 --- /dev/null +++ b/nltk/metrics/distance.py @@ -0,0 +1,553 @@ +# Natural Language Toolkit: Distance Metrics +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# Tom Lippincott +# URL: +# For license information, see LICENSE.TXT +# + +""" +Distance Metrics. + +Compute the distance between two items (usually strings). +As metrics, they must satisfy the following three requirements: + +1. d(a, a) = 0 +2. d(a, b) >= 0 +3. d(a, c) <= d(a, b) + d(b, c) +""" + +import operator +import warnings + +from nltk.pathsec import open as _secure_open + + +def _edit_dist_init(len1, len2): + lev = [] + for i in range(len1): + lev.append([0] * len2) # initialize 2D array to zero + for i in range(len1): + lev[i][0] = i # column 0: 0,1,2,3,4,... + for j in range(len2): + lev[0][j] = j # row 0: 0,1,2,3,4,... + return lev + + +def _last_left_t_init(sigma): + return {c: 0 for c in sigma} + + +def _edit_dist_step( + lev, i, j, s1, s2, last_left, last_right, substitution_cost=1, transpositions=False +): + c1 = s1[i - 1] + c2 = s2[j - 1] + + # skipping a character in s1 + a = lev[i - 1][j] + 1 + # skipping a character in s2 + b = lev[i][j - 1] + 1 + # substitution + c = lev[i - 1][j - 1] + (substitution_cost if c1 != c2 else 0) + + # transposition + d = c + 1 # never picked by default + if transpositions and last_left > 0 and last_right > 0: + d = lev[last_left - 1][last_right - 1] + i - last_left + j - last_right - 1 + + # pick the cheapest + lev[i][j] = min(a, b, c, d) + + +def edit_distance(s1, s2, substitution_cost=1, transpositions=False): + """ + Calculate the Levenshtein edit-distance between two strings. + The edit distance is the number of characters that need to be + substituted, inserted, or deleted, to transform s1 into s2. For + example, transforming "rain" to "shine" requires three steps, + consisting of two substitutions and one insertion: + "rain" -> "sain" -> "shin" -> "shine". These operations could have + been done in other orders, but at least three steps are needed. + + Allows specifying the cost of substitution edits (e.g., "a" -> "b"), + because sometimes it makes sense to assign greater penalties to + substitutions. + + This also optionally allows transposition edits (e.g., "ab" -> "ba"), + though this is disabled by default. + + :param s1, s2: The strings to be analysed + :param transpositions: Whether to allow transposition edits + :type s1: str + :type s2: str + :type substitution_cost: int + :type transpositions: bool + :rtype: int + """ + # set up a 2-D array + len1 = len(s1) + len2 = len(s2) + lev = _edit_dist_init(len1 + 1, len2 + 1) + + # retrieve alphabet + sigma = set() + sigma.update(s1) + sigma.update(s2) + + # set up table to remember positions of last seen occurrence in s1 + last_left_t = _last_left_t_init(sigma) + + # iterate over the array + # i and j start from 1 and not 0 to stay close to the wikipedia pseudo-code + # see https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance + for i in range(1, len1 + 1): + last_right_buf = 0 + for j in range(1, len2 + 1): + last_left = last_left_t[s2[j - 1]] + last_right = last_right_buf + if s1[i - 1] == s2[j - 1]: + last_right_buf = j + _edit_dist_step( + lev, + i, + j, + s1, + s2, + last_left, + last_right, + substitution_cost=substitution_cost, + transpositions=transpositions, + ) + last_left_t[s1[i - 1]] = i + return lev[len1][len2] + + +def _edit_dist_backtrace(lev, s1, s2, substitution_cost=1): + i, j = len(lev) - 1, len(lev[0]) - 1 + alignment = [(i, j)] + + while (i, j) != (0, 0): + directions = [ + (i - 1, j - 1), # substitution / match + (i - 1, j), # skip s1 + (i, j - 1), # skip s2 + ] + + direction_costs = [] + for pi, pj in directions: + if pi < 0 or pj < 0: + cost = float("inf") + elif pi == i - 1 and pj == j - 1: # diagonal + # Use actual transition cost: 0 for match, substitution_cost + # for mismatch. This ensures the backtrace prefers delete+insert + # (cost 2) over substitution when substitution_cost > 2. + sub_cost = 0 if s1[pi] == s2[pj] else substitution_cost + cost = lev[pi][pj] + sub_cost + else: # skip s1 or skip s2 + cost = lev[pi][pj] + 1 + direction_costs.append((cost, (pi, pj))) + + _, (i, j) = min(direction_costs, key=operator.itemgetter(0)) + + alignment.append((i, j)) + + return list(reversed(alignment)) + + +def edit_distance_align(s1, s2, substitution_cost=1): + """ + Calculate the minimum Levenshtein edit-distance based alignment + mapping between two strings. The alignment finds the mapping + from string s1 to s2 that minimizes the edit distance cost. + For example, mapping "rain" to "shine" would involve 2 + substitutions, 2 matches and an insertion resulting in + the following mapping: + [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (4, 5)] + NB: (0, 0) is the start state without any letters associated + See more: https://web.stanford.edu/class/cs124/lec/med.pdf + + In case of multiple valid minimum-distance alignments, the + backtrace has the following operation precedence: + + 1. Substitute s1 and s2 characters + 2. Skip s1 character + 3. Skip s2 character + + The backtrace is carried out in reverse string order. + + This function does not support transposition. + + :param s1, s2: The strings to be aligned + :type s1: str + :type s2: str + :type substitution_cost: int + :rtype: List[Tuple(int, int)] + """ + # set up a 2-D array + len1 = len(s1) + len2 = len(s2) + lev = _edit_dist_init(len1 + 1, len2 + 1) + + # iterate over the array + for i in range(len1): + for j in range(len2): + _edit_dist_step( + lev, + i + 1, + j + 1, + s1, + s2, + 0, + 0, + substitution_cost=substitution_cost, + transpositions=False, + ) + + # backtrace to find alignment + alignment = _edit_dist_backtrace(lev, s1, s2, substitution_cost) + return alignment + + +def binary_distance(label1, label2): + """Simple equality test. + + 0.0 if the labels are identical, 1.0 if they are different. + + >>> from nltk.metrics import binary_distance + >>> binary_distance(1,1) + 0.0 + + >>> binary_distance(1,3) + 1.0 + """ + + return 0.0 if label1 == label2 else 1.0 + + +def jaccard_distance(label1, label2): + """Distance metric comparing set-similarity.""" + return (len(label1.union(label2)) - len(label1.intersection(label2))) / len( + label1.union(label2) + ) + + +def masi_distance(label1, label2): + """Distance metric that takes into account partial agreement when multiple + labels are assigned. + + >>> from nltk.metrics import masi_distance + >>> masi_distance(set([1, 2]), set([1, 2, 3, 4])) + 0.665 + + Passonneau 2006, Measuring Agreement on Set-Valued Items (MASI) + for Semantic and Pragmatic Annotation. + """ + + len_intersection = len(label1.intersection(label2)) + len_union = len(label1.union(label2)) + len_label1 = len(label1) + len_label2 = len(label2) + if len_label1 == len_label2 and len_label1 == len_intersection: + m = 1 + elif len_intersection == min(len_label1, len_label2): + m = 0.67 + elif len_intersection > 0: + m = 0.33 + else: + m = 0 + + return 1 - len_intersection / len_union * m + + +def interval_distance(label1, label2): + """Krippendorff's interval distance metric + + >>> from nltk.metrics import interval_distance + >>> interval_distance(1,10) + 81 + + Krippendorff 1980, Content Analysis: An Introduction to its Methodology + """ + + try: + return pow(label1 - label2, 2) + # return pow(list(label1)[0]-list(label2)[0],2) + except Exception: + print("non-numeric labels not supported with interval distance") + + +def presence(label): + """Higher-order function to test presence of a given label""" + + return lambda x, y: 1.0 * ((label in x) == (label in y)) + + +def fractional_presence(label): + return ( + lambda x, y: abs((1.0 / len(x)) - (1.0 / len(y))) * (label in x and label in y) + or 0.0 * (label not in x and label not in y) + or abs(1.0 / len(x)) * (label in x and label not in y) + or (1.0 / len(y)) * (label not in x and label in y) + ) + + +def custom_distance(file): + data = {} + # Route through the pathsec sentinel so the read honours the file-access + # sandbox (allowed data roots, symlink resolution) instead of the builtin + # open, which bypasses it and can read arbitrary local files (CWE-22). + with _secure_open(file) as infile: + for l in infile: + labelA, labelB, dist = l.strip().split("\t") + labelA = frozenset([labelA]) + labelB = frozenset([labelB]) + data[frozenset([labelA, labelB])] = float(dist) + return lambda x, y: data[frozenset([x, y])] + + +def jaro_similarity(s1, s2): + """ + Computes the Jaro similarity between 2 sequences from: + + Matthew A. Jaro (1989). Advances in record linkage methodology + as applied to the 1985 census of Tampa Florida. Journal of the + American Statistical Association. 84 (406): 414-20. + + The Jaro distance between is the min no. of single-character transpositions + required to change one word into another. The Jaro similarity formula from + https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance : + + ``jaro_sim = 0 if m = 0 else 1/3 * (m/|s_1| + m/s_2 + (m-t)/m)`` + + where + - `|s_i|` is the length of string `s_i` + - `m` is the no. of matching characters + - `t` is the half no. of possible transpositions. + """ + # By definition, the similarity of a string with itself is 1.0. + # This also handles edge cases where both strings are empty or + # single-character identical strings, where the matching window + # formula (floor(max(|s1|,|s2|) / 2) - 1) yields -1. + if s1 == s2: + return 1.0 + + # First, store the length of the strings + # because they will be re-used several times. + len_s1, len_s2 = len(s1), len(s2) + + # The upper bound of the distance for being a matched character. + match_bound = max(len_s1, len_s2) // 2 - 1 + + # Initialize the counts for matches and transpositions. + matches = 0 # no.of matched characters in s1 and s2 + transpositions = 0 # no. of transpositions between s1 and s2 + flagged_1 = [] # positions in s1 which are matches to some character in s2 + # Positions in s2 which are matches to some character in s1, held as a set so + # the ``j not in matched_2`` membership test below is O(1): with a list it was + # O(len(matched_2)) inside the O(n**2) double loop, which grows to O(n**3) on + # near-matching strings and lets two short inputs pin a CPU core (CWE-770; + # CVE-2026-12926). + matched_2 = set() + + # Iterate through sequences, check for matches and compute transpositions. + for i in range(len_s1): # Iterate through each character. + upperbound = min(i + match_bound, len_s2 - 1) + lowerbound = max(0, i - match_bound) + for j in range(lowerbound, upperbound + 1): + if s1[i] == s2[j] and j not in matched_2: + matches += 1 + flagged_1.append(i) + matched_2.add(j) + break + # Ordered list of the matched s2 positions for the transposition pass, giving + # the same result as the original (which sorted the matched positions). + flagged_2 = sorted(matched_2) + for i, j in zip(flagged_1, flagged_2): + if s1[i] != s2[j]: + transpositions += 1 + + if matches == 0: + return 0.0 + else: + return ( + 1 + / 3 + * ( + matches / len_s1 + + matches / len_s2 + + (matches - transpositions // 2) / matches + ) + ) + + +def jaro_winkler_similarity(s1, s2, p=0.1, max_l=4): + """ + The Jaro Winkler distance is an extension of the Jaro similarity in: + + William E. Winkler. 1990. String Comparator Metrics and Enhanced + Decision Rules in the Fellegi-Sunter Model of Record Linkage. + Proceedings of the Section on Survey Research Methods. + American Statistical Association: 354-359. + + such that: + + jaro_winkler_sim = jaro_sim + ( l * p * (1 - jaro_sim) ) + + where, + + - jaro_sim is the output from the Jaro Similarity, + see jaro_similarity() + - l is the length of common prefix at the start of the string + - this implementation provides an upperbound for the l value + to keep the prefixes.A common value of this upperbound is 4. + - p is the constant scaling factor to overweigh common prefixes. + The Jaro-Winkler similarity will fall within the [0, 1] bound, + given that max(p)<=0.25 , default is p=0.1 in Winkler (1990) + + + Test using outputs from https://www.census.gov/srd/papers/pdf/rr93-8.pdf + from "Table 5 Comparison of String Comparators Rescaled between 0 and 1" + + >>> winkler_examples = [("billy", "billy"), ("billy", "bill"), ("billy", "blily"), + ... ("massie", "massey"), ("yvette", "yevett"), ("billy", "bolly"), ("dwayne", "duane"), + ... ("dixon", "dickson"), ("billy", "susan")] + + >>> winkler_scores = [1.000, 0.967, 0.947, 0.944, 0.911, 0.893, 0.858, 0.853, 0.000] + >>> jaro_scores = [1.000, 0.933, 0.933, 0.889, 0.889, 0.867, 0.822, 0.790, 0.000] + + One way to match the values on the Winkler's paper is to provide a different + p scaling factor for different pairs of strings, e.g. + + >>> p_factors = [0.1, 0.125, 0.20, 0.125, 0.20, 0.20, 0.20, 0.15, 0.1] + + >>> for (s1, s2), jscore, wscore, p in zip(winkler_examples, jaro_scores, winkler_scores, p_factors): + ... assert round(jaro_similarity(s1, s2), 3) == jscore + ... assert round(jaro_winkler_similarity(s1, s2, p=p), 3) == wscore + + + Test using outputs from https://www.census.gov/srd/papers/pdf/rr94-5.pdf from + "Table 2.1. Comparison of String Comparators Using Last Names, First Names, and Street Names" + + >>> winkler_examples = [('SHACKLEFORD', 'SHACKELFORD'), ('DUNNINGHAM', 'CUNNIGHAM'), + ... ('NICHLESON', 'NICHULSON'), ('JONES', 'JOHNSON'), ('MASSEY', 'MASSIE'), + ... ('ABROMS', 'ABRAMS'), ('HARDIN', 'MARTINEZ'), ('ITMAN', 'SMITH'), + ... ('JERALDINE', 'GERALDINE'), ('MARHTA', 'MARTHA'), ('MICHELLE', 'MICHAEL'), + ... ('JULIES', 'JULIUS'), ('TANYA', 'TONYA'), ('DWAYNE', 'DUANE'), ('SEAN', 'SUSAN'), + ... ('JON', 'JOHN'), ('JON', 'JAN'), ('BROOKHAVEN', 'BRROKHAVEN'), + ... ('BROOK HALLOW', 'BROOK HLLW'), ('DECATUR', 'DECATIR'), ('FITZRUREITER', 'FITZENREITER'), + ... ('HIGBEE', 'HIGHEE'), ('HIGBEE', 'HIGVEE'), ('LACURA', 'LOCURA'), ('IOWA', 'IONA'), ('1ST', 'IST')] + + >>> jaro_scores = [0.970, 0.896, 0.926, 0.790, 0.889, 0.889, 0.722, 0.467, 0.926, + ... 0.944, 0.869, 0.889, 0.867, 0.822, 0.783, 0.917, 0.000, 0.933, 0.944, 0.905, + ... 0.856, 0.889, 0.889, 0.889, 0.833, 0.000] + + >>> winkler_scores = [0.982, 0.896, 0.956, 0.832, 0.944, 0.922, 0.722, 0.467, 0.926, + ... 0.961, 0.921, 0.933, 0.880, 0.858, 0.805, 0.933, 0.000, 0.947, 0.967, 0.943, + ... 0.913, 0.922, 0.922, 0.900, 0.867, 0.000] + + One way to match the values on the Winkler's paper is to provide a different + p scaling factor for different pairs of strings, e.g. + + >>> p_factors = [0.1, 0.1, 0.1, 0.1, 0.125, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.20, + ... 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] + + + >>> for (s1, s2), jscore, wscore, p in zip(winkler_examples, jaro_scores, winkler_scores, p_factors): + ... if (s1, s2) in [('JON', 'JAN'), ('1ST', 'IST')]: + ... continue # Skip bad examples from the paper. + ... assert round(jaro_similarity(s1, s2), 3) == jscore + ... assert round(jaro_winkler_similarity(s1, s2, p=p), 3) == wscore + + + + This test-case proves that the output of Jaro-Winkler similarity depends on + the product l * p and not on the product max_l * p. Here the product max_l * p > 1 + however the product l * p <= 1 + + >>> round(jaro_winkler_similarity('TANYA', 'TONYA', p=0.1, max_l=100), 3) + 0.88 + + Test edge cases for very short or empty strings. + + >>> jaro_similarity("", "") == 1.0 + True + >>> jaro_similarity("", "nonempty") == 0.0 + True + >>> jaro_similarity("a", "a") == 1.0 + True + >>> jaro_similarity("a", "b") == 0.0 + True + >>> jaro_winkler_similarity("", "") == 1.0 + True + >>> jaro_winkler_similarity("a", "a") == 1.0 + True + """ + # To ensure that the output of the Jaro-Winkler's similarity + # falls between [0,1], the product of l * p needs to be + # also fall between [0,1]. + if not 0 <= max_l * p <= 1: + warnings.warn( + str( + "The product `max_l * p` might not fall between [0,1]." + "Jaro-Winkler similarity might not be between 0 and 1." + ) + ) + + # Compute the Jaro similarity + jaro_sim = jaro_similarity(s1, s2) + + # Initialize the upper bound for the no. of prefixes. + # if user did not pre-define the upperbound, + # use shorter length between s1 and s2 + + # Compute the prefix matches. + l = 0 + # zip() will automatically loop until the end of shorter string. + for s1_i, s2_i in zip(s1, s2): + if s1_i == s2_i: + l += 1 + else: + break + if l == max_l: + break + # Return the similarity value as described in docstring. + return jaro_sim + (l * p * (1 - jaro_sim)) + + +def demo(): + string_distance_examples = [ + ("rain", "shine"), + ("abcdef", "acbdef"), + ("language", "lnaguaeg"), + ("language", "lnaugage"), + ("language", "lngauage"), + ] + for s1, s2 in string_distance_examples: + print(f"Edit distance btwn '{s1}' and '{s2}':", edit_distance(s1, s2)) + print( + f"Edit dist with transpositions btwn '{s1}' and '{s2}':", + edit_distance(s1, s2, transpositions=True), + ) + print(f"Jaro similarity btwn '{s1}' and '{s2}':", jaro_similarity(s1, s2)) + print( + f"Jaro-Winkler similarity btwn '{s1}' and '{s2}':", + jaro_winkler_similarity(s1, s2), + ) + print( + f"Jaro-Winkler distance btwn '{s1}' and '{s2}':", + 1 - jaro_winkler_similarity(s1, s2), + ) + s1 = {1, 2, 3, 4} + s2 = {3, 4, 5} + print("s1:", s1) + print("s2:", s2) + print("Binary distance:", binary_distance(s1, s2)) + print("Jaccard distance:", jaccard_distance(s1, s2)) + print("MASI distance:", masi_distance(s1, s2)) + + +if __name__ == "__main__": + demo() diff --git a/nltk/metrics/paice.py b/nltk/metrics/paice.py new file mode 100644 index 0000000..7477eac --- /dev/null +++ b/nltk/metrics/paice.py @@ -0,0 +1,389 @@ +# Natural Language Toolkit: Agreement Metrics +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Lauri Hallila +# URL: +# For license information, see LICENSE.TXT +# + +"""Counts Paice's performance statistics for evaluating stemming algorithms. + +What is required: + - A dictionary of words grouped by their real lemmas + - A dictionary of words grouped by stems from a stemming algorithm + +When these are given, Understemming Index (UI), Overstemming Index (OI), +Stemming Weight (SW) and Error-rate relative to truncation (ERRT) are counted. + +References: +Chris D. Paice (1994). An evaluation method for stemming algorithms. +In Proceedings of SIGIR, 42--50. +""" + +from math import sqrt + + +def get_words_from_dictionary(lemmas): + """ + Get original set of words used for analysis. + + :param lemmas: A dictionary where keys are lemmas and values are sets + or lists of words corresponding to that lemma. + :type lemmas: dict(str): list(str) + :return: Set of words that exist as values in the dictionary + :rtype: set(str) + """ + words = set() + for lemma in lemmas: + words.update(set(lemmas[lemma])) + return words + + +def _truncate(words, cutlength): + """Group words by stems defined by truncating them at given length. + + :param words: Set of words used for analysis + :param cutlength: Words are stemmed by cutting at this length. + :type words: set(str) or list(str) + :type cutlength: int + :return: Dictionary where keys are stems and values are sets of words + corresponding to that stem. + :rtype: dict(str): set(str) + """ + stems = {} + for word in words: + stem = word[:cutlength] + try: + stems[stem].update([word]) + except KeyError: + stems[stem] = {word} + return stems + + +# Reference: https://en.wikipedia.org/wiki/Line-line_intersection +def _count_intersection(l1, l2): + """Count intersection between two line segments defined by coordinate pairs. + + :param l1: Tuple of two coordinate pairs defining the first line segment + :param l2: Tuple of two coordinate pairs defining the second line segment + :type l1: tuple(float, float) + :type l2: tuple(float, float) + :return: Coordinates of the intersection + :rtype: tuple(float, float) + """ + x1, y1 = l1[0] + x2, y2 = l1[1] + x3, y3 = l2[0] + x4, y4 = l2[1] + + denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + + if denominator == 0.0: # lines are parallel + if x1 == x2 == x3 == x4 == 0.0: + # When lines are parallel, they must be on the y-axis. + # We can ignore x-axis because we stop counting the + # truncation line when we get there. + # There are no other options as UI (x-axis) grows and + # OI (y-axis) diminishes when we go along the truncation line. + return (0.0, y4) + + x = ( + (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4) + ) / denominator + y = ( + (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4) + ) / denominator + return (x, y) + + +def _get_derivative(coordinates): + """Get derivative of the line from (0,0) to given coordinates. + + :param coordinates: A coordinate pair + :type coordinates: tuple(float, float) + :return: Derivative; inf if x is zero + :rtype: float + """ + try: + return coordinates[1] / coordinates[0] + except ZeroDivisionError: + return float("inf") + + +def _calculate_cut(lemmawords, stems): + """Count understemmed and overstemmed pairs for (lemma, stem) pair with common words. + + :param lemmawords: Set or list of words corresponding to certain lemma. + :param stems: A dictionary where keys are stems and values are sets + or lists of words corresponding to that stem. + :type lemmawords: set(str) or list(str) + :type stems: dict(str): set(str) + :return: Amount of understemmed and overstemmed pairs contributed by words + existing in both lemmawords and stems. + :rtype: tuple(float, float) + """ + umt, wmt = 0.0, 0.0 + for stem in stems: + cut = set(lemmawords) & set(stems[stem]) + if cut: + cutcount = len(cut) + stemcount = len(stems[stem]) + # Unachieved merge total + umt += cutcount * (len(lemmawords) - cutcount) + # Wrongly merged total + wmt += cutcount * (stemcount - cutcount) + return (umt, wmt) + + +def _calculate(lemmas, stems): + """Calculate actual and maximum possible amounts of understemmed and overstemmed word pairs. + + :param lemmas: A dictionary where keys are lemmas and values are sets + or lists of words corresponding to that lemma. + :param stems: A dictionary where keys are stems and values are sets + or lists of words corresponding to that stem. + :type lemmas: dict(str): list(str) + :type stems: dict(str): set(str) + :return: Global unachieved merge total (gumt), + global desired merge total (gdmt), + global wrongly merged total (gwmt) and + global desired non-merge total (gdnt). + :rtype: tuple(float, float, float, float) + """ + + n = sum(len(lemmas[word]) for word in lemmas) + + gdmt, gdnt, gumt, gwmt = (0.0, 0.0, 0.0, 0.0) + + for lemma in lemmas: + lemmacount = len(lemmas[lemma]) + + # Desired merge total + gdmt += lemmacount * (lemmacount - 1) + + # Desired non-merge total + gdnt += lemmacount * (n - lemmacount) + + # For each (lemma, stem) pair with common words, count how many + # pairs are understemmed and overstemmed. + umt, wmt = _calculate_cut(lemmas[lemma], stems) + + # Add to total undesired and wrongly-merged totals + gumt += umt + gwmt += wmt + + # Each object is counted twice, so divide by two + return (gumt / 2, gdmt / 2, gwmt / 2, gdnt / 2) + + +def _indexes(gumt, gdmt, gwmt, gdnt): + """Count Understemming Index (UI), Overstemming Index (OI) and Stemming Weight (SW). + + :param gumt, gdmt, gwmt, gdnt: Global unachieved merge total (gumt), + global desired merge total (gdmt), + global wrongly merged total (gwmt) and + global desired non-merge total (gdnt). + :type gumt, gdmt, gwmt, gdnt: float + :return: Understemming Index (UI), + Overstemming Index (OI) and + Stemming Weight (SW). + :rtype: tuple(float, float, float) + """ + # Calculate Understemming Index (UI), + # Overstemming Index (OI) and Stemming Weight (SW) + try: + ui = gumt / gdmt + except ZeroDivisionError: + # If GDMT (max merge total) is 0, define UI as 0 + ui = 0.0 + try: + oi = gwmt / gdnt + except ZeroDivisionError: + # IF GDNT (max non-merge total) is 0, define OI as 0 + oi = 0.0 + try: + sw = oi / ui + except ZeroDivisionError: + if oi == 0.0: + # OI and UI are 0, define SW as 'not a number' + sw = float("nan") + else: + # UI is 0, define SW as infinity + sw = float("inf") + return (ui, oi, sw) + + +class Paice: + """Class for storing lemmas, stems and evaluation metrics.""" + + def __init__(self, lemmas, stems): + """ + :param lemmas: A dictionary where keys are lemmas and values are sets + or lists of words corresponding to that lemma. + :param stems: A dictionary where keys are stems and values are sets + or lists of words corresponding to that stem. + :type lemmas: dict(str): list(str) + :type stems: dict(str): set(str) + """ + self.lemmas = lemmas + self.stems = stems + self.coords = [] + self.gumt, self.gdmt, self.gwmt, self.gdnt = (None, None, None, None) + self.ui, self.oi, self.sw = (None, None, None) + self.errt = None + self.update() + + def __str__(self): + text = ["Global Unachieved Merge Total (GUMT): %s\n" % self.gumt] + text.append("Global Desired Merge Total (GDMT): %s\n" % self.gdmt) + text.append("Global Wrongly-Merged Total (GWMT): %s\n" % self.gwmt) + text.append("Global Desired Non-merge Total (GDNT): %s\n" % self.gdnt) + text.append("Understemming Index (GUMT / GDMT): %s\n" % self.ui) + text.append("Overstemming Index (GWMT / GDNT): %s\n" % self.oi) + text.append("Stemming Weight (OI / UI): %s\n" % self.sw) + text.append("Error-Rate Relative to Truncation (ERRT): %s\r\n" % self.errt) + coordinates = " ".join(["(%s, %s)" % item for item in self.coords]) + text.append("Truncation line: %s" % coordinates) + return "".join(text) + + def _get_truncation_indexes(self, words, cutlength): + """Count (UI, OI) when stemming is done by truncating words at \'cutlength\'. + + :param words: Words used for the analysis + :param cutlength: Words are stemmed by cutting them at this length + :type words: set(str) or list(str) + :type cutlength: int + :return: Understemming and overstemming indexes + :rtype: tuple(int, int) + """ + + truncated = _truncate(words, cutlength) + gumt, gdmt, gwmt, gdnt = _calculate(self.lemmas, truncated) + ui, oi = _indexes(gumt, gdmt, gwmt, gdnt)[:2] + return (ui, oi) + + def _get_truncation_coordinates(self, cutlength=0): + """Count (UI, OI) pairs for truncation points until we find the segment where (ui, oi) crosses the truncation line. + + :param cutlength: Optional parameter to start counting from (ui, oi) + coordinates gotten by stemming at this length. Useful for speeding up + the calculations when you know the approximate location of the + intersection. + :type cutlength: int + :return: List of coordinate pairs that define the truncation line + :rtype: list(tuple(float, float)) + """ + words = get_words_from_dictionary(self.lemmas) + maxlength = max(len(word) for word in words) + + # Truncate words from different points until (0, 0) - (ui, oi) segment crosses the truncation line + coords = [] + while cutlength <= maxlength: + # Get (UI, OI) pair of current truncation point + pair = self._get_truncation_indexes(words, cutlength) + + # Store only new coordinates so we'll have an actual + # line segment when counting the intersection point + if pair not in coords: + coords.append(pair) + if pair == (0.0, 0.0): + # Stop counting if truncation line goes through origo; + # length from origo to truncation line is 0 + return coords + if len(coords) >= 2 and pair[0] > 0.0: + derivative1 = _get_derivative(coords[-2]) + derivative2 = _get_derivative(coords[-1]) + # Derivative of the truncation line is a decreasing value; + # when it passes Stemming Weight, we've found the segment + # of truncation line intersecting with (0, 0) - (ui, oi) segment + if derivative1 >= self.sw >= derivative2: + return coords + cutlength += 1 + return coords + + def _errt(self): + """Count Error-Rate Relative to Truncation (ERRT). + + :return: ERRT, length of the line from origo to (UI, OI) divided by + the length of the line from origo to the point defined by the same + line when extended until the truncation line. + :rtype: float + """ + # Count (UI, OI) pairs for truncation points until we find the segment where (ui, oi) crosses the truncation line + self.coords = self._get_truncation_coordinates() + if (0.0, 0.0) in self.coords: + # Truncation line goes through origo, so ERRT cannot be counted + if (self.ui, self.oi) != (0.0, 0.0): + return float("inf") + else: + return float("nan") + if (self.ui, self.oi) == (0.0, 0.0): + # (ui, oi) is origo; define errt as 0.0 + return 0.0 + # Count the intersection point + # Note that (self.ui, self.oi) cannot be (0.0, 0.0) and self.coords has different coordinates + # so we have actual line segments instead of a line segment and a point + intersection = _count_intersection( + ((0, 0), (self.ui, self.oi)), self.coords[-2:] + ) + # Count OP (length of the line from origo to (ui, oi)) + op = sqrt(self.ui**2 + self.oi**2) + # Count OT (length of the line from origo to truncation line that goes through (ui, oi)) + ot = sqrt(intersection[0] ** 2 + intersection[1] ** 2) + # OP / OT tells how well the stemming algorithm works compared to just truncating words + return op / ot + + def update(self): + """Update statistics after lemmas and stems have been set.""" + self.gumt, self.gdmt, self.gwmt, self.gdnt = _calculate(self.lemmas, self.stems) + self.ui, self.oi, self.sw = _indexes(self.gumt, self.gdmt, self.gwmt, self.gdnt) + self.errt = self._errt() + + +def demo(): + """Demonstration of the module.""" + # Some words with their real lemmas + lemmas = { + "kneel": ["kneel", "knelt"], + "range": ["range", "ranged"], + "ring": ["ring", "rang", "rung"], + } + # Same words with stems from a stemming algorithm + stems = { + "kneel": ["kneel"], + "knelt": ["knelt"], + "rang": ["rang", "range", "ranged"], + "ring": ["ring"], + "rung": ["rung"], + } + print("Words grouped by their lemmas:") + for lemma in sorted(lemmas): + print("{} => {}".format(lemma, " ".join(lemmas[lemma]))) + print() + print("Same words grouped by a stemming algorithm:") + for stem in sorted(stems): + print("{} => {}".format(stem, " ".join(stems[stem]))) + print() + p = Paice(lemmas, stems) + print(p) + print() + # Let's "change" results from a stemming algorithm + stems = { + "kneel": ["kneel"], + "knelt": ["knelt"], + "rang": ["rang"], + "range": ["range", "ranged"], + "ring": ["ring"], + "rung": ["rung"], + } + print("Counting stats after changing stemming results:") + for stem in sorted(stems): + print("{} => {}".format(stem, " ".join(stems[stem]))) + print() + p.stems = stems + p.update() + print(p) + + +if __name__ == "__main__": + demo() diff --git a/nltk/metrics/scores.py b/nltk/metrics/scores.py new file mode 100644 index 0000000..2b66cad --- /dev/null +++ b/nltk/metrics/scores.py @@ -0,0 +1,228 @@ +# Natural Language Toolkit: Evaluation +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +import operator +from functools import reduce +from math import fabs +from random import shuffle + +try: + from scipy.stats.stats import betai +except ImportError: + betai = None + +from nltk.util import LazyConcatenation, LazyMap + + +def accuracy(reference, test): + """ + Given a list of reference values and a corresponding list of test + values, return the fraction of corresponding values that are + equal. In particular, return the fraction of indices + ``0= actual_stat: + c += 1 + + if verbose and i % 10 == 0: + print("pseudo-statistic: %f" % pseudo_stat) + print("significance: %f" % ((c + 1) / (i + 1))) + print("-" * 60) + + significance = (c + 1) / (shuffles + 1) + + if verbose: + print("significance: %f" % significance) + if betai: + for phi in [0.01, 0.05, 0.10, 0.15, 0.25, 0.50]: + print(f"prob(phi<={phi:f}): {betai(c, shuffles, phi):f}") + + return (significance, c, shuffles) + + +def demo(): + print("-" * 75) + reference = "DET NN VB DET JJ NN NN IN DET NN".split() + test = "DET VB VB DET NN NN NN IN DET NN".split() + print("Reference =", reference) + print("Test =", test) + print("Accuracy:", accuracy(reference, test)) + + print("-" * 75) + reference_set = set(reference) + test_set = set(test) + print("Reference =", reference_set) + print("Test = ", test_set) + print("Precision:", precision(reference_set, test_set)) + print(" Recall:", recall(reference_set, test_set)) + print("F-Measure:", f_measure(reference_set, test_set)) + print("-" * 75) + + +if __name__ == "__main__": + demo() diff --git a/nltk/metrics/segmentation.py b/nltk/metrics/segmentation.py new file mode 100644 index 0000000..4a8c959 --- /dev/null +++ b/nltk/metrics/segmentation.py @@ -0,0 +1,267 @@ +# Natural Language Toolkit: Text Segmentation Metrics +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# David Doukhan +# URL: +# For license information, see LICENSE.TXT + + +""" +Text Segmentation Metrics + +1. Windowdiff + +Pevzner, L., and Hearst, M., A Critique and Improvement of + an Evaluation Metric for Text Segmentation, + Computational Linguistics 28, 19-36 + + +2. Generalized Hamming Distance + +Bookstein A., Kulyukin V.A., Raita T. +Generalized Hamming Distance +Information Retrieval 5, 2002, pp 353-375 + +Baseline implementation in C++ +http://digital.cs.usu.edu/~vkulyukin/vkweb/software/ghd/ghd.html + +Study describing benefits of Generalized Hamming Distance Versus +WindowDiff for evaluating text segmentation tasks +Begsten, Y. Quel indice pour mesurer l'efficacite en segmentation de textes ? +TALN 2009 + + +3. Pk text segmentation metric + +Beeferman D., Berger A., Lafferty J. (1999) +Statistical Models for Text Segmentation +Machine Learning, 34, 177-210 +""" + +try: + import numpy as np +except ImportError: + pass + + +def windowdiff(seg1, seg2, k, boundary="1", weighted=False): + """ + Compute the windowdiff score for a pair of segmentations. A + segmentation is any sequence over a vocabulary of two items + (e.g. "0", "1"), where the specified boundary value is used to + mark the edge of a segmentation. + + From Pevzner & Hearst (2002), the WindowDiff metric is defined as:: + + WindowDiff(ref, hyp, k) = + 1 / (N - k) * sum_{i=1}^{N-k} ( + |b(ref, i, i+k) - b(hyp, i, i+k)| > 0 + ) + + where ``b(seg, i, j)`` counts the number of boundaries in ``seg`` + between positions ``i`` and ``j``, and ``N = len(seg)``. + + The weighted variant sums the absolute differences instead + of thresholding at 1. + + >>> s1 = "000100000010" + >>> s2 = "000010000100" + >>> s3 = "100000010000" + >>> '%.2f' % windowdiff(s1, s1, 3) + '0.00' + >>> '%.2f' % windowdiff(s1, s2, 3) + '0.30' + >>> '%.2f' % windowdiff(s2, s3, 3) + '0.80' + + :param seg1: a segmentation + :type seg1: str or list + :param seg2: a segmentation + :type seg2: str or list + :param k: window width + :type k: int + :param boundary: boundary value + :type boundary: str or int or bool + :param weighted: use the weighted variant of windowdiff + :type weighted: boolean + :rtype: float + """ + + if len(seg1) != len(seg2): + raise ValueError("Segmentations have unequal length") + if k < 0: + raise ValueError("Window width k should not be negative") + if k > len(seg1): + raise ValueError( + "Window width k should be smaller or equal than segmentation lengths" + ) + wd = 0 + # Maintain the boundary counts for the sliding window incrementally rather + # than recomputing seg[i:i+k].count(boundary) from scratch at every position + # (which is O(k) per step and makes the metric O(n*k) -- quadratic when the + # window k is proportional to the segmentation length). + count1 = seg1[:k].count(boundary) + count2 = seg2[:k].count(boundary) + for i in range(len(seg1) - k + 1): + if i > 0: + # The window moved one position right in seg1 and seg2: drop index + # i-1 and add index i+k-1. + count1 += (seg1[i + k - 1] == boundary) - (seg1[i - 1] == boundary) + count2 += (seg2[i + k - 1] == boundary) - (seg2[i - 1] == boundary) + ndiff = abs(count1 - count2) + if weighted: + wd += ndiff + else: + wd += min(1, ndiff) + return wd / (len(seg1) - k + 1.0) + + +# Generalized Hamming Distance + + +def _init_mat(nrows, ncols, ins_cost, del_cost): + mat = np.empty((nrows, ncols)) + mat[0, :] = ins_cost * np.arange(ncols) + mat[:, 0] = del_cost * np.arange(nrows) + return mat + + +def _ghd_aux(mat, rowv, colv, ins_cost, del_cost, shift_cost_coeff): + for i, rowi in enumerate(rowv): + for j, colj in enumerate(colv): + shift_cost = shift_cost_coeff * abs(rowi - colj) + mat[i, j] + if rowi == colj: + # boundaries are at the same location, no transformation required + tcost = mat[i, j] + elif rowi > colj: + # boundary match through a deletion + tcost = del_cost + mat[i, j + 1] + else: + # boundary match through an insertion + tcost = ins_cost + mat[i + 1, j] + mat[i + 1, j + 1] = min(tcost, shift_cost) + + +def ghd(ref, hyp, ins_cost=2.0, del_cost=2.0, shift_cost_coeff=1.0, boundary="1"): + """ + Compute the Generalized Hamming Distance for a reference and a hypothetical + segmentation, corresponding to the cost related to the transformation + of the hypothetical segmentation into the reference segmentation + through boundary insertion, deletion and shift operations. + + A segmentation is any sequence over a vocabulary of two items + (e.g. "0", "1"), where the specified boundary value is used to + mark the edge of a segmentation. + + Recommended parameter values are a shift_cost_coeff of 2. + Associated with a ins_cost, and del_cost equal to the mean segment + length in the reference segmentation. + + >>> # Same examples as Kulyukin C++ implementation + >>> ghd('1100100000', '1100010000', 1.0, 1.0, 0.5) + 0.5 + >>> ghd('1100100000', '1100000001', 1.0, 1.0, 0.5) + 2.0 + >>> ghd('011', '110', 1.0, 1.0, 0.5) + 1.0 + >>> ghd('1', '0', 1.0, 1.0, 0.5) + 1.0 + >>> ghd('111', '000', 1.0, 1.0, 0.5) + 3.0 + >>> ghd('000', '111', 1.0, 2.0, 0.5) + 6.0 + + :param ref: the reference segmentation + :type ref: str or list + :param hyp: the hypothetical segmentation + :type hyp: str or list + :param ins_cost: insertion cost + :type ins_cost: float + :param del_cost: deletion cost + :type del_cost: float + :param shift_cost_coeff: constant used to compute the cost of a shift. + ``shift cost = shift_cost_coeff * |i - j|`` where ``i`` and ``j`` + are the positions indicating the shift + :type shift_cost_coeff: float + :param boundary: boundary value + :type boundary: str or int or bool + :rtype: float + """ + + ref_idx = [i for (i, val) in enumerate(ref) if val == boundary] + hyp_idx = [i for (i, val) in enumerate(hyp) if val == boundary] + + nref_bound = len(ref_idx) + nhyp_bound = len(hyp_idx) + + if nref_bound == 0 and nhyp_bound == 0: + return 0.0 + elif nref_bound > 0 and nhyp_bound == 0: + return nref_bound * ins_cost + elif nref_bound == 0 and nhyp_bound > 0: + return nhyp_bound * del_cost + + mat = _init_mat(nhyp_bound + 1, nref_bound + 1, ins_cost, del_cost) + _ghd_aux(mat, hyp_idx, ref_idx, ins_cost, del_cost, shift_cost_coeff) + return float(mat[-1, -1]) + + +# Beeferman's Pk text segmentation evaluation metric + + +def pk(ref, hyp, k=None, boundary="1"): + """ + Compute the Pk metric for a pair of segmentations A segmentation + is any sequence over a vocabulary of two items (e.g. "0", "1"), + where the specified boundary value is used to mark the edge of a + segmentation. + + >>> '%.2f' % pk('0100'*100, '1'*400, 2) + '0.50' + >>> '%.2f' % pk('0100'*100, '0'*400, 2) + '0.50' + >>> '%.2f' % pk('0100'*100, '0100'*100, 2) + '0.00' + + :param ref: the reference segmentation + :type ref: str or list + :param hyp: the segmentation to evaluate + :type hyp: str or list + :param k: window size, if None, set to half of the average reference segment length + :type boundary: str or int or bool + :param boundary: boundary value + :type boundary: str or int or bool + :rtype: float + """ + + if len(ref) != len(hyp): + raise ValueError("Segmentations have unequal length") + if k is None: + # Half the average reference segment length. A boundary-free reference + # has a count of 0, which would make this an uncaught ZeroDivisionError + # (CWE-369); treat it as a single segment (count >= 1) so the metric is + # still computed instead of crashing. + k = int(round(len(ref) / (max(ref.count(boundary), 1) * 2.0))) + if k < 0: + raise ValueError("Window width k should not be negative") + + err = 0 + # Maintain the boundary counts for the sliding window incrementally rather + # than recomputing ref/hyp[i:i+k].count(boundary) from scratch at every + # position (which is O(k) per step and makes the metric O(n*k) -- quadratic, + # since k is ~ half the average segment length). + ref_count = ref[:k].count(boundary) + hyp_count = hyp[:k].count(boundary) + for i in range(len(ref) - k + 1): + if i > 0: + # The window moved one position right in ref and hyp: drop index + # i-1 and add index i+k-1. + ref_count += (ref[i + k - 1] == boundary) - (ref[i - 1] == boundary) + hyp_count += (hyp[i + k - 1] == boundary) - (hyp[i - 1] == boundary) + r = ref_count > 0 + h = hyp_count > 0 + if r != h: + err += 1 + return err / (len(ref) - k + 1.0) diff --git a/nltk/metrics/spearman.py b/nltk/metrics/spearman.py new file mode 100644 index 0000000..4127f71 --- /dev/null +++ b/nltk/metrics/spearman.py @@ -0,0 +1,68 @@ +# Natural Language Toolkit: Spearman Rank Correlation +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Joel Nothman +# URL: +# For license information, see LICENSE.TXT + +""" +Tools for comparing ranked lists. +""" + + +def _rank_dists(ranks1, ranks2): + """Finds the difference between the values in ranks1 and ranks2 for keys + present in both dicts. If the arguments are not dicts, they are converted + from (key, rank) sequences. + """ + ranks1 = dict(ranks1) + ranks2 = dict(ranks2) + for k in ranks1: + try: + yield k, ranks1[k] - ranks2[k] + except KeyError: + pass + + +def spearman_correlation(ranks1, ranks2): + """Returns the Spearman correlation coefficient for two rankings, which + should be dicts or sequences of (key, rank). The coefficient ranges from + -1.0 (ranks are opposite) to 1.0 (ranks are identical), and is only + calculated for keys in both rankings (for meaningful results, remove keys + present in only one list before ranking).""" + n = 0 + res = 0 + for k, d in _rank_dists(ranks1, ranks2): + res += d * d + n += 1 + try: + return 1 - (6 * res / (n * (n * n - 1))) + except ZeroDivisionError: + # Result is undefined if only one item is ranked + return 0.0 + + +def ranks_from_sequence(seq): + """Given a sequence, yields each element with an increasing rank, suitable + for use as an argument to ``spearman_correlation``. + """ + return ((k, i) for i, k in enumerate(seq)) + + +def ranks_from_scores(scores, rank_gap=1e-15): + """Given a sequence of (key, score) tuples, yields each key with an + increasing rank, tying with previous key's rank if the difference between + their scores is less than rank_gap. Suitable for use as an argument to + ``spearman_correlation``. + """ + prev_score = None + rank = 0 + for i, (key, score) in enumerate(scores): + try: + if abs(score - prev_score) > rank_gap: + rank = i + except TypeError: + pass + + yield key, rank + prev_score = score diff --git a/nltk/misc/__init__.py b/nltk/misc/__init__.py new file mode 100644 index 0000000..60011b1 --- /dev/null +++ b/nltk/misc/__init__.py @@ -0,0 +1,11 @@ +# Natural Language Toolkit: Miscellaneous modules +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from nltk.misc.babelfish import babelize_shell +from nltk.misc.chomsky import generate_chomsky +from nltk.misc.minimalset import MinimalSet +from nltk.misc.wordfinder import word_finder diff --git a/nltk/misc/babelfish.py b/nltk/misc/babelfish.py new file mode 100644 index 0000000..a43fd4d --- /dev/null +++ b/nltk/misc/babelfish.py @@ -0,0 +1,10 @@ +""" +This module previously provided an interface to Babelfish online +translation service; this service is no longer available; this +module is kept in NLTK source code in order to provide better error +messages for people following the NLTK Book 2.0. +""" + + +def babelize_shell(): + print("Babelfish online translation service is no longer available.") diff --git a/nltk/misc/chomsky.py b/nltk/misc/chomsky.py new file mode 100644 index 0000000..297d20c --- /dev/null +++ b/nltk/misc/chomsky.py @@ -0,0 +1,134 @@ +# Chomsky random text generator, version 1.1, Raymond Hettinger, 2005/09/13 +# https://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440546 + +""" +CHOMSKY is an aid to writing linguistic papers in the style +of the great master. It is based on selected phrases taken +from actual books and articles written by Noam Chomsky. +Upon request, it assembles the phrases in the elegant +stylistic patterns that Chomsky is noted for. +To generate n sentences of linguistic wisdom, type + + (CHOMSKY n) -- for example + (CHOMSKY 5) generates half a screen of linguistic truth. +""" + +leadins = """To characterize a linguistic level L, + On the other hand, + This suggests that + It appears that + Furthermore, + We will bring evidence in favor of the following thesis: + To provide a constituent structure for T(Z,K), + From C1, it follows that + For any transformation which is sufficiently diversified in \ +application to be of any interest, + Analogously, + Clearly, + Note that + Of course, + Suppose, for instance, that + Thus + With this clarification, + Conversely, + We have already seen that + By combining adjunctions and certain deformations, + I suggested that these results would follow from the assumption that + If the position of the trace in (99c) were only relatively \ +inaccessible to movement, + However, this assumption is not correct, since + Comparing these examples with their parasitic gap counterparts in \ +(96) and (97), we see that + In the discussion of resumptive pronouns following (81), + So far, + Nevertheless, + For one thing, + Summarizing, then, we assume that + A consequence of the approach just outlined is that + Presumably, + On our assumptions, + It may be, then, that + It must be emphasized, once again, that + Let us continue to suppose that + Notice, incidentally, that """ +# List of LEADINs to buy time. + +subjects = """ the notion of level of grammaticalness + a case of semigrammaticalness of a different sort + most of the methodological work in modern linguistics + a subset of English sentences interesting on quite independent grounds + the natural general principle that will subsume this case + an important property of these three types of EC + any associated supporting element + the appearance of parasitic gaps in domains relatively inaccessible \ +to ordinary extraction + the speaker-hearer's linguistic intuition + the descriptive power of the base component + the earlier discussion of deviance + this analysis of a formative as a pair of sets of features + this selectionally introduced contextual feature + a descriptively adequate grammar + the fundamental error of regarding functional notions as categorial + relational information + the systematic use of complex symbols + the theory of syntactic features developed earlier""" +# List of SUBJECTs chosen for maximum professorial macho. + +verbs = """can be defined in such a way as to impose + delimits + suffices to account for + cannot be arbitrary in + is not subject to + does not readily tolerate + raises serious doubts about + is not quite equivalent to + does not affect the structure of + may remedy and, at the same time, eliminate + is not to be considered in determining + is to be regarded as + is unspecified with respect to + is, apparently, determined by + is necessary to impose an interpretation on + appears to correlate rather closely with + is rather different from""" +# List of VERBs chosen for autorecursive obfuscation. + +objects = """ problems of phonemic and morphological analysis. + a corpus of utterance tokens upon which conformity has been defined \ +by the paired utterance test. + the traditional practice of grammarians. + the levels of acceptability from fairly high (e.g. (99a)) to virtual \ +gibberish (e.g. (98d)). + a stipulation to place the constructions into these various categories. + a descriptive fact. + a parasitic gap construction. + the extended c-command discussed in connection with (34). + the ultimate standard that determines the accuracy of any proposed grammar. + the system of base rules exclusive of the lexicon. + irrelevant intervening contexts in selectional rules. + nondistinctness in the sense of distinctive feature theory. + a general convention regarding the forms of the grammar. + an abstract underlying order. + an important distinction in language use. + the requirement that branching is not tolerated within the dominance \ +scope of a complex symbol. + the strong generative capacity of the theory.""" +# List of OBJECTs selected for profound sententiousness. + +import random +import textwrap +from itertools import chain, islice + + +def generate_chomsky(times=5, line_length=72): + parts = [] + for part in (leadins, subjects, verbs, objects): + phraselist = list(map(str.strip, part.splitlines())) + random.shuffle(phraselist) + parts.append(phraselist) + output = chain.from_iterable(islice(zip(*parts), 0, times)) + print(textwrap.fill(" ".join(output), line_length)) + + +if __name__ == "__main__": + generate_chomsky() diff --git a/nltk/misc/minimalset.py b/nltk/misc/minimalset.py new file mode 100644 index 0000000..5449714 --- /dev/null +++ b/nltk/misc/minimalset.py @@ -0,0 +1,85 @@ +# Natural Language Toolkit: Minimal Sets +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from collections import defaultdict + + +class MinimalSet: + """ + Find contexts where more than one possible target value can + appear. E.g. if targets are word-initial letters, and contexts + are the remainders of words, then we would like to find cases like + "fat" vs "cat", and "training" vs "draining". If targets are + parts-of-speech and contexts are words, then we would like to find + cases like wind (noun) 'air in rapid motion', vs wind (verb) + 'coil, wrap'. + """ + + def __init__(self, parameters=None): + """ + Create a new minimal set. + + :param parameters: The (context, target, display) tuples for the item + :type parameters: list(tuple(str, str, str)) + """ + self._targets = set() # the contrastive information + self._contexts = set() # what we are controlling for + self._seen = defaultdict(set) # to record what we have seen + self._displays = {} # what we will display + + if parameters: + for context, target, display in parameters: + self.add(context, target, display) + + def add(self, context, target, display): + """ + Add a new item to the minimal set, having the specified + context, target, and display form. + + :param context: The context in which the item of interest appears + :type context: str + :param target: The item of interest + :type target: str + :param display: The information to be reported for each item + :type display: str + """ + # Store the set of targets that occurred in this context + self._seen[context].add(target) + + # Keep track of which contexts and targets we have seen + self._contexts.add(context) + self._targets.add(target) + + # For a given context and target, store the display form + self._displays[(context, target)] = display + + def contexts(self, minimum=2): + """ + Determine which contexts occurred with enough distinct targets. + + :param minimum: the minimum number of distinct target forms + :type minimum: int + :rtype: list + """ + return [c for c in self._contexts if len(self._seen[c]) >= minimum] + + def display(self, context, target, default=""): + if (context, target) in self._displays: + return self._displays[(context, target)] + else: + return default + + def display_all(self, context): + result = [] + for target in self._targets: + x = self.display(context, target) + if x: + result.append(x) + return result + + def targets(self): + return self._targets diff --git a/nltk/misc/sort.py b/nltk/misc/sort.py new file mode 100644 index 0000000..4949ec6 --- /dev/null +++ b/nltk/misc/sort.py @@ -0,0 +1,176 @@ +# Natural Language Toolkit: List Sorting +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +This module provides a variety of list sorting algorithms, to +illustrate the many different algorithms (recipes) for solving a +problem, and how to analyze algorithms experimentally. +""" +# These algorithms are taken from: +# Levitin (2004) The Design and Analysis of Algorithms + +################################################################## +# Selection Sort +################################################################## + + +def selection(a): + """ + Selection Sort: scan the list to find its smallest element, then + swap it with the first element. The remainder of the list is one + element smaller; apply the same method to this list, and so on. + """ + count = 0 + + for i in range(len(a) - 1): + min = i + + for j in range(i + 1, len(a)): + if a[j] < a[min]: + min = j + + count += 1 + + a[min], a[i] = a[i], a[min] + + return count + + +################################################################## +# Bubble Sort +################################################################## + + +def bubble(a): + """ + Bubble Sort: compare adjacent elements of the list left-to-right, + and swap them if they are out of order. After one pass through + the list swapping adjacent items, the largest item will be in + the rightmost position. The remainder is one element smaller; + apply the same method to this list, and so on. + """ + count = 0 + for i in range(len(a) - 1): + for j in range(len(a) - i - 1): + if a[j + 1] < a[j]: + a[j], a[j + 1] = a[j + 1], a[j] + count += 1 + return count + + +################################################################## +# Merge Sort +################################################################## + + +def _merge_lists(b, c): + count = 0 + i = j = 0 + a = [] + while i < len(b) and j < len(c): + count += 1 + if b[i] <= c[j]: + a.append(b[i]) + i += 1 + else: + a.append(c[j]) + j += 1 + if i == len(b): + a += c[j:] + else: + a += b[i:] + return a, count + + +def merge(a): + """ + Merge Sort: split the list in half, and sort each half, then + combine the sorted halves. + """ + count = 0 + if len(a) > 1: + midpoint = len(a) // 2 + b = a[:midpoint] + c = a[midpoint:] + count_b = merge(b) + count_c = merge(c) + result, count_a = _merge_lists(b, c) + a[:] = result # copy the result back into a. + count = count_a + count_b + count_c + return count + + +################################################################## +# Quick Sort +################################################################## + + +def _partition(a, l, r): + p = a[l] + i = l + j = r + 1 + count = 0 + while True: + while i < r: + i += 1 + if a[i] >= p: + break + while j > l: + j -= 1 + if j < l or a[j] <= p: + break + a[i], a[j] = a[j], a[i] # swap + count += 1 + if i >= j: + break + a[i], a[j] = a[j], a[i] # undo last swap + a[l], a[j] = a[j], a[l] + return j, count + + +def _quick(a, l, r): + count = 0 + if l < r: + s, count = _partition(a, l, r) + count += _quick(a, l, s - 1) + count += _quick(a, s + 1, r) + return count + + +def quick(a): + return _quick(a, 0, len(a) - 1) + + +################################################################## +# Demonstration +################################################################## + + +def demo(): + from random import shuffle + + for size in (10, 20, 50, 100, 200, 500, 1000): + a = list(range(size)) + + # various sort methods + shuffle(a) + count_selection = selection(a) + shuffle(a) + count_bubble = bubble(a) + shuffle(a) + count_merge = merge(a) + shuffle(a) + count_quick = quick(a) + + print( + ("size=%5d: selection=%8d, bubble=%8d, " "merge=%6d, quick=%6d") + % (size, count_selection, count_bubble, count_merge, count_quick) + ) + + +if __name__ == "__main__": + demo() diff --git a/nltk/misc/wordfinder.py b/nltk/misc/wordfinder.py new file mode 100644 index 0000000..bb38a2d --- /dev/null +++ b/nltk/misc/wordfinder.py @@ -0,0 +1,139 @@ +# Natural Language Toolkit: Word Finder +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# URL: +# For license information, see LICENSE.TXT + +# Simplified from PHP version by Robert Klein +# http://fswordfinder.sourceforge.net/ + +import random + + +# reverse a word with probability 0.5 +def revword(word): + if random.randint(1, 2) == 1: + return word[::-1] + return word + + +# try to insert word at position x,y; direction encoded in xf,yf +def step(word, x, xf, y, yf, grid): + for i in range(len(word)): + if grid[xf(i)][yf(i)] != "" and grid[xf(i)][yf(i)] != word[i]: + return False + for i in range(len(word)): + grid[xf(i)][yf(i)] = word[i] + return True + + +# try to insert word at position x,y, in direction dir +def check(word, dir, x, y, grid, rows, cols): + if dir == 1: + if x - len(word) < 0 or y - len(word) < 0: + return False + return step(word, x, lambda i: x - i, y, lambda i: y - i, grid) + elif dir == 2: + if x - len(word) < 0: + return False + return step(word, x, lambda i: x - i, y, lambda i: y, grid) + elif dir == 3: + if x - len(word) < 0 or y + (len(word) - 1) >= cols: + return False + return step(word, x, lambda i: x - i, y, lambda i: y + i, grid) + elif dir == 4: + if y - len(word) < 0: + return False + return step(word, x, lambda i: x, y, lambda i: y - i, grid) + + +def wordfinder(words, rows=20, cols=20, attempts=50, alph="ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + """ + Attempt to arrange words into a letter-grid with the specified + number of rows and columns. Try each word in several positions + and directions, until it can be fitted into the grid, or the + maximum number of allowable attempts is exceeded. Returns a tuple + consisting of the grid and the words that were successfully + placed. + + :param words: the list of words to be put into the grid + :type words: list + :param rows: the number of rows in the grid + :type rows: int + :param cols: the number of columns in the grid + :type cols: int + :param attempts: the number of times to attempt placing a word + :type attempts: int + :param alph: the alphabet, to be used for filling blank cells + :type alph: list + :rtype: tuple + """ + + # place longer words first + words = sorted(words, key=len, reverse=True) + + grid = [] # the letter grid + used = [] # the words we used + + # initialize the grid + for i in range(rows): + grid.append([""] * cols) + + # try to place each word + for word in words: + word = word.strip().upper() # normalize + save = word # keep a record of the word + word = revword(word) + for attempt in range(attempts): + r = random.randint(0, len(word)) + dir = random.choice([1, 2, 3, 4]) + x = random.randint(0, rows) + y = random.randint(0, cols) + if dir == 1: + x += r + y += r + elif dir == 2: + x += r + elif dir == 3: + x += r + y -= r + elif dir == 4: + y += r + if 0 <= x < rows and 0 <= y < cols: + if check(word, dir, x, y, grid, rows, cols): + # used.append((save, dir, x, y, word)) + used.append(save) + break + + # Fill up the remaining spaces + for i in range(rows): + for j in range(cols): + if grid[i][j] == "": + grid[i][j] = random.choice(alph) + + return grid, used + + +def word_finder(): + from nltk.corpus import words + + wordlist = words.words() + random.shuffle(wordlist) + wordlist = wordlist[:200] + wordlist = [w for w in wordlist if 3 <= len(w) <= 12] + grid, used = wordfinder(wordlist) + + print("Word Finder\n") + for i in range(len(grid)): + for j in range(len(grid[i])): + print(grid[i][j], end=" ") + print() + print() + + for i in range(len(used)): + print("%d:" % (i + 1), used[i]) + + +if __name__ == "__main__": + word_finder() diff --git a/nltk/parse/__init__.py b/nltk/parse/__init__.py new file mode 100644 index 0000000..56118a4 --- /dev/null +++ b/nltk/parse/__init__.py @@ -0,0 +1,102 @@ +# Natural Language Toolkit: Parsers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT +# + +""" +NLTK Parsers + +Classes and interfaces for producing tree structures that represent +the internal organization of a text. This task is known as "parsing" +the text, and the resulting tree structures are called the text's +"parses". Typically, the text is a single sentence, and the tree +structure represents the syntactic structure of the sentence. +However, parsers can also be used in other domains. For example, +parsers can be used to derive the morphological structure of the +morphemes that make up a word, or to derive the discourse structure +for a set of utterances. + +Sometimes, a single piece of text can be represented by more than one +tree structure. Texts represented by more than one tree structure are +called "ambiguous" texts. Note that there are actually two ways in +which a text can be ambiguous: + + - The text has multiple correct parses. + - There is not enough information to decide which of several + candidate parses is correct. + +However, the parser module does *not* distinguish these two types of +ambiguity. + +The parser module defines ``ParserI``, a standard interface for parsing +texts; and two simple implementations of that interface, +``ShiftReduceParser`` and ``RecursiveDescentParser``. It also contains +three sub-modules for specialized kinds of parsing: + + - ``nltk.parser.chart`` defines chart parsing, which uses dynamic + programming to efficiently parse texts. + - ``nltk.parser.probabilistic`` defines probabilistic parsing, which + associates a probability with each parse. +""" + +from nltk.parse.api import ParserI +from nltk.parse.bllip import BllipParser +from nltk.parse.chart import ( + BottomUpChartParser, + BottomUpLeftCornerChartParser, + ChartParser, + LeftCornerChartParser, + SteppingChartParser, + TopDownChartParser, +) +from nltk.parse.corenlp import CoreNLPDependencyParser, CoreNLPParser +from nltk.parse.dependencygraph import DependencyGraph +from nltk.parse.earleychart import ( + EarleyChartParser, + FeatureEarleyChartParser, + FeatureIncrementalBottomUpChartParser, + FeatureIncrementalBottomUpLeftCornerChartParser, + FeatureIncrementalChartParser, + FeatureIncrementalTopDownChartParser, + IncrementalBottomUpChartParser, + IncrementalBottomUpLeftCornerChartParser, + IncrementalChartParser, + IncrementalLeftCornerChartParser, + IncrementalTopDownChartParser, +) +from nltk.parse.evaluate import DependencyEvaluator +from nltk.parse.featurechart import ( + FeatureBottomUpChartParser, + FeatureBottomUpLeftCornerChartParser, + FeatureChartParser, + FeatureTopDownChartParser, +) +from nltk.parse.malt import MaltParser +from nltk.parse.nonprojectivedependencyparser import ( + NaiveBayesDependencyScorer, + NonprojectiveDependencyParser, + ProbabilisticNonprojectiveParser, +) +from nltk.parse.pchart import ( + BottomUpProbabilisticChartParser, + InsideChartParser, + LongestChartParser, + RandomChartParser, + UnsortedChartParser, +) +from nltk.parse.projectivedependencyparser import ( + ProbabilisticProjectiveDependencyParser, + ProjectiveDependencyParser, +) +from nltk.parse.recursivedescent import ( + RecursiveDescentParser, + SteppingRecursiveDescentParser, +) +from nltk.parse.shiftreduce import ShiftReduceParser, SteppingShiftReduceParser +from nltk.parse.transitionparser import TransitionParser +from nltk.parse.util import TestGrammar, extract_test_sentences, load_parser +from nltk.parse.viterbi import ViterbiParser diff --git a/nltk/parse/api.py b/nltk/parse/api.py new file mode 100644 index 0000000..d79e2f4 --- /dev/null +++ b/nltk/parse/api.py @@ -0,0 +1,72 @@ +# Natural Language Toolkit: Parser API +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# URL: +# For license information, see LICENSE.TXT +# + +import itertools + +from nltk.internals import overridden + + +class ParserI: + """ + A processing class for deriving trees that represent possible + structures for a sequence of tokens. These tree structures are + known as "parses". Typically, parsers are used to derive syntax + trees for sentences. But parsers can also be used to derive other + kinds of tree structure, such as morphological trees and discourse + structures. + + Subclasses must define: + - at least one of: ``parse()``, ``parse_sents()``. + + Subclasses may define: + - ``grammar()`` + """ + + def grammar(self): + """ + :return: The grammar used by this parser. + """ + raise NotImplementedError() + + def parse(self, sent, *args, **kwargs): + """ + :return: An iterator that generates parse trees for the sentence. + When possible this list is sorted from most likely to least likely. + + :param sent: The sentence to be parsed + :type sent: list(str) + :rtype: iter(Tree) + """ + if overridden(self.parse_sents): + return next(self.parse_sents([sent], *args, **kwargs)) + elif overridden(self.parse_one): + return ( + tree + for tree in [self.parse_one(sent, *args, **kwargs)] + if tree is not None + ) + elif overridden(self.parse_all): + return iter(self.parse_all(sent, *args, **kwargs)) + else: + raise NotImplementedError() + + def parse_sents(self, sents, *args, **kwargs): + """ + Apply ``self.parse()`` to each element of ``sents``. + :rtype: iter(iter(Tree)) + """ + return (self.parse(sent, *args, **kwargs) for sent in sents) + + def parse_all(self, sent, *args, **kwargs): + """:rtype: list(Tree)""" + return list(self.parse(sent, *args, **kwargs)) + + def parse_one(self, sent, *args, **kwargs): + """:rtype: Tree or None""" + return next(self.parse(sent, *args, **kwargs), None) diff --git a/nltk/parse/bllip.py b/nltk/parse/bllip.py new file mode 100644 index 0000000..b993424 --- /dev/null +++ b/nltk/parse/bllip.py @@ -0,0 +1,299 @@ +# Natural Language Toolkit: Interface to BLLIP Parser +# +# Author: David McClosky +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +from nltk.parse.api import ParserI +from nltk.tree import Tree + +""" +Interface for parsing with BLLIP Parser. Requires the Python +bllipparser module. BllipParser objects can be constructed with the +``BllipParser.from_unified_model_dir`` class method or manually using the +``BllipParser`` constructor. The former is generally easier if you have +a BLLIP Parser unified model directory -- a basic model can be obtained +from NLTK's downloader. More unified parsing models can be obtained with +BLLIP Parser's ModelFetcher (run ``python -m bllipparser.ModelFetcher`` +or see docs for ``bllipparser.ModelFetcher.download_and_install_model``). + +Basic usage:: + + # download and install a basic unified parsing model (Wall Street Journal) + # sudo python -m nltk.downloader bllip_wsj_no_aux + + >>> from nltk.data import find + >>> model_dir = find('models/bllip_wsj_no_aux').path + >>> bllip = BllipParser.from_unified_model_dir(model_dir) + + # 1-best parsing + >>> sentence1 = 'British left waffles on Falklands .'.split() + >>> top_parse = bllip.parse_one(sentence1) + >>> print(top_parse) + (S1 + (S + (NP (JJ British) (NN left)) + (VP (VBZ waffles) (PP (IN on) (NP (NNP Falklands)))) + (. .))) + + # n-best parsing + >>> sentence2 = 'Time flies'.split() + >>> all_parses = bllip.parse_all(sentence2) + >>> print(len(all_parses)) + 50 + >>> print(all_parses[0]) + (S1 (S (NP (NNP Time)) (VP (VBZ flies)))) + + # incorporating external tagging constraints (None means unconstrained tag) + >>> constrained1 = bllip.tagged_parse([('Time', 'VB'), ('flies', 'NNS')]) + >>> print(next(constrained1)) + (S1 (NP (VB Time) (NNS flies))) + >>> constrained2 = bllip.tagged_parse([('Time', 'NN'), ('flies', None)]) + >>> print(next(constrained2)) + (S1 (NP (NN Time) (VBZ flies))) + +References +---------- + +- Charniak, Eugene. "A maximum-entropy-inspired parser." Proceedings of + the 1st North American chapter of the Association for Computational + Linguistics conference. Association for Computational Linguistics, + 2000. + +- Charniak, Eugene, and Mark Johnson. "Coarse-to-fine n-best parsing + and MaxEnt discriminative reranking." Proceedings of the 43rd Annual + Meeting on Association for Computational Linguistics. Association + for Computational Linguistics, 2005. + +Known issues +------------ + +Note that BLLIP Parser is not currently threadsafe. Since this module +uses a SWIG interface, it is potentially unsafe to create multiple +``BllipParser`` objects in the same process. BLLIP Parser currently +has issues with non-ASCII text and will raise an error if given any. + +See https://pypi.python.org/pypi/bllipparser/ for more information +on BLLIP Parser's Python interface. +""" + +__all__ = ["BllipParser"] + +# this block allows this module to be imported even if bllipparser isn't +# available +try: + from bllipparser import RerankingParser + from bllipparser.RerankingParser import get_unified_model_parameters + + def _ensure_bllip_import_or_error(): + pass + +except ImportError as ie: + + def _ensure_bllip_import_or_error(ie=ie): + raise ImportError("Couldn't import bllipparser module: %s" % ie) + + +def _ensure_ascii(words): + try: + for i, word in enumerate(words): + word.encode("ascii") + except UnicodeEncodeError as e: + raise ValueError( + f"Token {i} ({word!r}) is non-ASCII. BLLIP Parser " + "currently doesn't support non-ASCII inputs." + ) from e + + +def _scored_parse_to_nltk_tree(scored_parse): + return Tree.fromstring(str(scored_parse.ptb_parse)) + + +class BllipParser(ParserI): + """ + Interface for parsing with BLLIP Parser. BllipParser objects can be + constructed with the ``BllipParser.from_unified_model_dir`` class + method or manually using the ``BllipParser`` constructor. + """ + + def __init__( + self, + parser_model=None, + reranker_features=None, + reranker_weights=None, + parser_options=None, + reranker_options=None, + ): + """ + Load a BLLIP Parser model from scratch. You'll typically want to + use the ``from_unified_model_dir()`` class method to construct + this object. + + :param parser_model: Path to parser model directory + :type parser_model: str + + :param reranker_features: Path the reranker model's features file + :type reranker_features: str + + :param reranker_weights: Path the reranker model's weights file + :type reranker_weights: str + + :param parser_options: optional dictionary of parser options, see + ``bllipparser.RerankingParser.RerankingParser.load_parser_options()`` + for more information. + :type parser_options: dict(str) + + :param reranker_options: optional + dictionary of reranker options, see + ``bllipparser.RerankingParser.RerankingParser.load_reranker_model()`` + for more information. + :type reranker_options: dict(str) + """ + _ensure_bllip_import_or_error() + + parser_options = parser_options or {} + reranker_options = reranker_options or {} + + self.rrp = RerankingParser() + self.rrp.load_parser_model(parser_model, **parser_options) + if reranker_features and reranker_weights: + self.rrp.load_reranker_model( + features_filename=reranker_features, + weights_filename=reranker_weights, + **reranker_options, + ) + + def parse(self, sentence): + """ + Use BLLIP Parser to parse a sentence. Takes a sentence as a list + of words; it will be automatically tagged with this BLLIP Parser + instance's tagger. + + :return: An iterator that generates parse trees for the sentence + from most likely to least likely. + + :param sentence: The sentence to be parsed + :type sentence: list(str) + :rtype: iter(Tree) + """ + _ensure_ascii(sentence) + nbest_list = self.rrp.parse(sentence) + for scored_parse in nbest_list: + yield _scored_parse_to_nltk_tree(scored_parse) + + def tagged_parse(self, word_and_tag_pairs): + """ + Use BLLIP to parse a sentence. Takes a sentence as a list of + (word, tag) tuples; the sentence must have already been tokenized + and tagged. BLLIP will attempt to use the tags provided but may + use others if it can't come up with a complete parse subject + to those constraints. You may also specify a tag as ``None`` + to leave a token's tag unconstrained. + + :return: An iterator that generates parse trees for the sentence + from most likely to least likely. + + :param sentence: Input sentence to parse as (word, tag) pairs + :type sentence: list(tuple(str, str)) + :rtype: iter(Tree) + """ + words = [] + tag_map = {} + for i, (word, tag) in enumerate(word_and_tag_pairs): + words.append(word) + if tag is not None: + tag_map[i] = tag + + _ensure_ascii(words) + nbest_list = self.rrp.parse_tagged(words, tag_map) + for scored_parse in nbest_list: + yield _scored_parse_to_nltk_tree(scored_parse) + + @classmethod + def from_unified_model_dir( + cls, model_dir, parser_options=None, reranker_options=None + ): + """ + Create a ``BllipParser`` object from a unified parsing model + directory. Unified parsing model directories are a standardized + way of storing BLLIP parser and reranker models together on disk. + See ``bllipparser.RerankingParser.get_unified_model_parameters()`` + for more information about unified model directories. + + :return: A ``BllipParser`` object using the parser and reranker + models in the model directory. + + :param model_dir: Path to the unified model directory. + :type model_dir: str + :param parser_options: optional dictionary of parser options, see + ``bllipparser.RerankingParser.RerankingParser.load_parser_options()`` + for more information. + :type parser_options: dict(str) + :param reranker_options: optional dictionary of reranker options, see + ``bllipparser.RerankingParser.RerankingParser.load_reranker_model()`` + for more information. + :type reranker_options: dict(str) + :rtype: BllipParser + """ + ( + parser_model_dir, + reranker_features_filename, + reranker_weights_filename, + ) = get_unified_model_parameters(model_dir) + return cls( + parser_model_dir, + reranker_features_filename, + reranker_weights_filename, + parser_options, + reranker_options, + ) + + +def demo(): + """This assumes the Python module bllipparser is installed.""" + + # download and install a basic unified parsing model (Wall Street Journal) + # sudo python -m nltk.downloader bllip_wsj_no_aux + + from nltk.data import find + + model_dir = find("models/bllip_wsj_no_aux").path + + print("Loading BLLIP Parsing models...") + # the easiest way to get started is to use a unified model + bllip = BllipParser.from_unified_model_dir(model_dir) + print("Done.") + + sentence1 = "British left waffles on Falklands .".split() + sentence2 = "I saw the man with the telescope .".split() + # this sentence is known to fail under the WSJ parsing model + fail1 = "# ! ? : -".split() + for sentence in (sentence1, sentence2, fail1): + print("Sentence: %r" % " ".join(sentence)) + try: + tree = next(bllip.parse(sentence)) + print(tree) + except StopIteration: + print("(parse failed)") + + # n-best parsing demo + for i, parse in enumerate(bllip.parse(sentence1)): + print("parse %d:\n%s" % (i, parse)) + + # using external POS tag constraints + print( + "forcing 'tree' to be 'NN':", + next(bllip.tagged_parse([("A", None), ("tree", "NN")])), + ) + print( + "forcing 'A' to be 'DT' and 'tree' to be 'NNP':", + next(bllip.tagged_parse([("A", "DT"), ("tree", "NNP")])), + ) + # constraints don't have to make sense... (though on more complicated + # sentences, they may cause the parse to fail) + print( + "forcing 'A' to be 'NNP':", + next(bllip.tagged_parse([("A", "NNP"), ("tree", None)])), + ) diff --git a/nltk/parse/broker_test.cfg b/nltk/parse/broker_test.cfg new file mode 100644 index 0000000..5e8e595 --- /dev/null +++ b/nltk/parse/broker_test.cfg @@ -0,0 +1,9 @@ +%start S + +S[sem=] -> NP[sem=?subj] VP[sem=?vp] +VP[sem = ] -> V[sem = ?v] NP[sem=?obj] +VP[sem = ?v] -> V[sem = ?v] +NP[sem = ] -> 'Kim' +NP[sem = ] -> 'I' +V[sem = <\x y.(like x y)>, tns=pres] -> 'like' +V[sem = <\x.(sleeps x)>, tns=pres] -> 'sleeps' diff --git a/nltk/parse/chart.py b/nltk/parse/chart.py new file mode 100644 index 0000000..bd8b0b8 --- /dev/null +++ b/nltk/parse/chart.py @@ -0,0 +1,1907 @@ +# Natural Language Toolkit: A Chart Parser +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# Jean Mark Gawron +# Peter Ljunglöf +# URL: +# For license information, see LICENSE.TXT + +""" +Data classes and parser implementations for "chart parsers", which +use dynamic programming to efficiently parse a text. A chart +parser derives parse trees for a text by iteratively adding "edges" +to a "chart." Each edge represents a hypothesis about the tree +structure for a subsequence of the text. The chart is a +"blackboard" for composing and combining these hypotheses. + +When a chart parser begins parsing a text, it creates a new (empty) +chart, spanning the text. It then incrementally adds new edges to the +chart. A set of "chart rules" specifies the conditions under which +new edges should be added to the chart. Once the chart reaches a +stage where none of the chart rules adds any new edges, parsing is +complete. + +Charts are encoded with the ``Chart`` class, and edges are encoded with +the ``TreeEdge`` and ``LeafEdge`` classes. The chart parser module +defines three chart parsers: + + - ``ChartParser`` is a simple and flexible chart parser. Given a + set of chart rules, it will apply those rules to the chart until + no more edges are added. + + - ``SteppingChartParser`` is a subclass of ``ChartParser`` that can + be used to step through the parsing process. +""" + +import itertools +import re +import warnings +from functools import total_ordering + +from nltk.grammar import PCFG, is_nonterminal, is_terminal +from nltk.internals import raise_unorderable_types +from nltk.parse.api import ParserI +from nltk.tree import Tree +from nltk.util import OrderedDict + +######################################################################## +## Edges +######################################################################## + + +@total_ordering +class EdgeI: + """ + A hypothesis about the structure of part of a sentence. + Each edge records the fact that a structure is (partially) + consistent with the sentence. An edge contains: + + - A span, indicating what part of the sentence is + consistent with the hypothesized structure. + - A left-hand side, specifying what kind of structure is + hypothesized. + - A right-hand side, specifying the contents of the + hypothesized structure. + - A dot position, indicating how much of the hypothesized + structure is consistent with the sentence. + + Every edge is either complete or incomplete: + + - An edge is complete if its structure is fully consistent + with the sentence. + - An edge is incomplete if its structure is partially + consistent with the sentence. For every incomplete edge, the + span specifies a possible prefix for the edge's structure. + + There are two kinds of edge: + + - A ``TreeEdge`` records which trees have been found to + be (partially) consistent with the text. + - A ``LeafEdge`` records the tokens occurring in the text. + + The ``EdgeI`` interface provides a common interface to both types + of edge, allowing chart parsers to treat them in a uniform manner. + """ + + def __init__(self): + if self.__class__ == EdgeI: + raise TypeError("Edge is an abstract interface") + + # //////////////////////////////////////////////////////////// + # Span + # //////////////////////////////////////////////////////////// + + def span(self): + """ + Return a tuple ``(s, e)``, where ``tokens[s:e]`` is the + portion of the sentence that is consistent with this + edge's structure. + + :rtype: tuple(int, int) + """ + raise NotImplementedError() + + def start(self): + """ + Return the start index of this edge's span. + + :rtype: int + """ + raise NotImplementedError() + + def end(self): + """ + Return the end index of this edge's span. + + :rtype: int + """ + raise NotImplementedError() + + def length(self): + """ + Return the length of this edge's span. + + :rtype: int + """ + raise NotImplementedError() + + # //////////////////////////////////////////////////////////// + # Left Hand Side + # //////////////////////////////////////////////////////////// + + def lhs(self): + """ + Return this edge's left-hand side, which specifies what kind + of structure is hypothesized by this edge. + + :see: ``TreeEdge`` and ``LeafEdge`` for a description of + the left-hand side values for each edge type. + """ + raise NotImplementedError() + + # //////////////////////////////////////////////////////////// + # Right Hand Side + # //////////////////////////////////////////////////////////// + + def rhs(self): + """ + Return this edge's right-hand side, which specifies + the content of the structure hypothesized by this edge. + + :see: ``TreeEdge`` and ``LeafEdge`` for a description of + the right-hand side values for each edge type. + """ + raise NotImplementedError() + + def dot(self): + """ + Return this edge's dot position, which indicates how much of + the hypothesized structure is consistent with the + sentence. In particular, ``self.rhs[:dot]`` is consistent + with ``tokens[self.start():self.end()]``. + + :rtype: int + """ + raise NotImplementedError() + + def nextsym(self): + """ + Return the element of this edge's right-hand side that + immediately follows its dot. + + :rtype: Nonterminal or terminal or None + """ + raise NotImplementedError() + + def is_complete(self): + """ + Return True if this edge's structure is fully consistent + with the text. + + :rtype: bool + """ + raise NotImplementedError() + + def is_incomplete(self): + """ + Return True if this edge's structure is partially consistent + with the text. + + :rtype: bool + """ + raise NotImplementedError() + + # //////////////////////////////////////////////////////////// + # Comparisons & hashing + # //////////////////////////////////////////////////////////// + + def __eq__(self, other): + return ( + self.__class__ is other.__class__ + and self._comparison_key == other._comparison_key + ) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, EdgeI): + raise_unorderable_types("<", self, other) + if self.__class__ is other.__class__: + return self._comparison_key < other._comparison_key + else: + return self.__class__.__name__ < other.__class__.__name__ + + def __hash__(self): + try: + return self._hash + except AttributeError: + self._hash = hash(self._comparison_key) + return self._hash + + +class TreeEdge(EdgeI): + """ + An edge that records the fact that a tree is (partially) + consistent with the sentence. A tree edge consists of: + + - A span, indicating what part of the sentence is + consistent with the hypothesized tree. + - A left-hand side, specifying the hypothesized tree's node + value. + - A right-hand side, specifying the hypothesized tree's + children. Each element of the right-hand side is either a + terminal, specifying a token with that terminal as its leaf + value; or a nonterminal, specifying a subtree with that + nonterminal's symbol as its node value. + - A dot position, indicating which children are consistent + with part of the sentence. In particular, if ``dot`` is the + dot position, ``rhs`` is the right-hand size, ``(start,end)`` + is the span, and ``sentence`` is the list of tokens in the + sentence, then ``tokens[start:end]`` can be spanned by the + children specified by ``rhs[:dot]``. + + For more information about edges, see the ``EdgeI`` interface. + """ + + def __init__(self, span, lhs, rhs, dot=0): + """ + Construct a new ``TreeEdge``. + + :type span: tuple(int, int) + :param span: A tuple ``(s, e)``, where ``tokens[s:e]`` is the + portion of the sentence that is consistent with the new + edge's structure. + :type lhs: Nonterminal + :param lhs: The new edge's left-hand side, specifying the + hypothesized tree's node value. + :type rhs: list(Nonterminal and str) + :param rhs: The new edge's right-hand side, specifying the + hypothesized tree's children. + :type dot: int + :param dot: The position of the new edge's dot. This position + specifies what prefix of the production's right hand side + is consistent with the text. In particular, if + ``sentence`` is the list of tokens in the sentence, then + ``okens[span[0]:span[1]]`` can be spanned by the + children specified by ``rhs[:dot]``. + """ + self._span = span + self._lhs = lhs + rhs = tuple(rhs) + self._rhs = rhs + self._dot = dot + self._comparison_key = (span, lhs, rhs, dot) + + @staticmethod + def from_production(production, index): + """ + Return a new ``TreeEdge`` formed from the given production. + The new edge's left-hand side and right-hand side will + be taken from ``production``; its span will be + ``(index,index)``; and its dot position will be ``0``. + + :rtype: TreeEdge + """ + return TreeEdge( + span=(index, index), lhs=production.lhs(), rhs=production.rhs(), dot=0 + ) + + def move_dot_forward(self, new_end): + """ + Return a new ``TreeEdge`` formed from this edge. + The new edge's dot position is increased by ``1``, + and its end index will be replaced by ``new_end``. + + :param new_end: The new end index. + :type new_end: int + :rtype: TreeEdge + """ + return TreeEdge( + span=(self._span[0], new_end), + lhs=self._lhs, + rhs=self._rhs, + dot=self._dot + 1, + ) + + # Accessors + def lhs(self): + return self._lhs + + def span(self): + return self._span + + def start(self): + return self._span[0] + + def end(self): + return self._span[1] + + def length(self): + return self._span[1] - self._span[0] + + def rhs(self): + return self._rhs + + def dot(self): + return self._dot + + def is_complete(self): + return self._dot == len(self._rhs) + + def is_incomplete(self): + return self._dot != len(self._rhs) + + def nextsym(self): + if self._dot >= len(self._rhs): + return None + else: + return self._rhs[self._dot] + + # String representation + def __str__(self): + str = f"[{self._span[0]}:{self._span[1]}] " + str += "%-2r ->" % (self._lhs,) + + for i in range(len(self._rhs)): + if i == self._dot: + str += " *" + str += " %s" % repr(self._rhs[i]) + if len(self._rhs) == self._dot: + str += " *" + return str + + def __repr__(self): + return "[Edge: %s]" % self + + +class LeafEdge(EdgeI): + """ + An edge that records the fact that a leaf value is consistent with + a word in the sentence. A leaf edge consists of: + + - An index, indicating the position of the word. + - A leaf, specifying the word's content. + + A leaf edge's left-hand side is its leaf value, and its right hand + side is ``()``. Its span is ``[index, index+1]``, and its dot + position is ``0``. + """ + + def __init__(self, leaf, index): + """ + Construct a new ``LeafEdge``. + + :param leaf: The new edge's leaf value, specifying the word + that is recorded by this edge. + :param index: The new edge's index, specifying the position of + the word that is recorded by this edge. + """ + self._leaf = leaf + self._index = index + self._comparison_key = (leaf, index) + + # Accessors + def lhs(self): + return self._leaf + + def span(self): + return (self._index, self._index + 1) + + def start(self): + return self._index + + def end(self): + return self._index + 1 + + def length(self): + return 1 + + def rhs(self): + return () + + def dot(self): + return 0 + + def is_complete(self): + return True + + def is_incomplete(self): + return False + + def nextsym(self): + return None + + # String representations + def __str__(self): + return f"[{self._index}:{self._index + 1}] {repr(self._leaf)}" + + def __repr__(self): + return "[Edge: %s]" % (self) + + +######################################################################## +## Chart +######################################################################## + +#: Upper bound on the number of parse-tree nodes a single tree extraction +#: (:meth:`Chart.trees`) may build. A highly-ambiguous grammar (e.g. the +#: 15-byte ``S -> S S | 'a'``) makes the number of parses exponential in the +#: sentence length (the Catalan numbers), and the trees are materialised eagerly +#: -- so a tiny grammar plus a short sentence pins the CPU and exhausts memory, +#: and even obtaining the first parse does not return (CWE-770; CVE-2026-12886). +#: Once this many tree nodes have been built, extraction raises ``ValueError`` +#: instead of running unbounded. Raise it if you legitimately need to enumerate +#: a more ambiguous parse forest. +MAX_PARSE_TREES = 1_000_000 + + +class _ParseTreeBudget: + """Counts constructed parse-tree nodes and aborts runaway tree extraction. + + One budget is shared across a whole :meth:`Chart.trees` call (including the + recursive subtree builds and the memoised reuse), so it bounds the total + work of reading an exponential parse forest off the chart. + """ + + __slots__ = ("remaining", "limit") + + def __init__(self, limit): + self.remaining = limit + self.limit = limit + + def spend(self): + self.remaining -= 1 + if self.remaining < 0: + raise ValueError( + "Refusing to extract parse trees: a highly-ambiguous grammar can " + "yield an exponential number of parses, and tree extraction " + "exceeded the limit of %d parse-tree nodes (CWE-770). Parse with " + "a less ambiguous grammar or a shorter sentence, or raise " + "nltk.parse.chart.MAX_PARSE_TREES." % self.limit + ) + + +class Chart: + """ + A blackboard for hypotheses about the syntactic constituents of a + sentence. A chart contains a set of edges, and each edge encodes + a single hypothesis about the structure of some portion of the + sentence. + + The ``select`` method can be used to select a specific collection + of edges. For example ``chart.select(is_complete=True, start=0)`` + yields all complete edges whose start indices are 0. To ensure + the efficiency of these selection operations, ``Chart`` dynamically + creates and maintains an index for each set of attributes that + have been selected on. + + In order to reconstruct the trees that are represented by an edge, + the chart associates each edge with a set of child pointer lists. + A child pointer list is a list of the edges that license an + edge's right-hand side. + + :ivar _tokens: The sentence that the chart covers. + :ivar _num_leaves: The number of tokens. + :ivar _edges: A list of the edges in the chart + :ivar _edge_to_cpls: A dictionary mapping each edge to a set + of child pointer lists that are associated with that edge. + :ivar _indexes: A dictionary mapping tuples of edge attributes + to indices, where each index maps the corresponding edge + attribute values to lists of edges. + """ + + def __init__(self, tokens): + """ + Construct a new chart. The chart is initialized with the + leaf edges corresponding to the terminal leaves. + + :type tokens: list + :param tokens: The sentence that this chart will be used to parse. + """ + # Record the sentence token and the sentence length. + self._tokens = tuple(tokens) + self._num_leaves = len(self._tokens) + + # Initialise the chart. + self.initialize() + + def initialize(self): + """ + Clear the chart. + """ + # A list of edges contained in this chart. + self._edges = [] + + # The set of child pointer lists associated with each edge. + self._edge_to_cpls = {} + + # Indexes mapping attribute values to lists of edges + # (used by select()). + self._indexes = {} + + # //////////////////////////////////////////////////////////// + # Sentence Access + # //////////////////////////////////////////////////////////// + + def num_leaves(self): + """ + Return the number of words in this chart's sentence. + + :rtype: int + """ + return self._num_leaves + + def leaf(self, index): + """ + Return the leaf value of the word at the given index. + + :rtype: str + """ + return self._tokens[index] + + def leaves(self): + """ + Return a list of the leaf values of each word in the + chart's sentence. + + :rtype: list(str) + """ + return self._tokens + + # //////////////////////////////////////////////////////////// + # Edge access + # //////////////////////////////////////////////////////////// + + def edges(self): + """ + Return a list of all edges in this chart. New edges + that are added to the chart after the call to edges() + will *not* be contained in this list. + + :rtype: list(EdgeI) + :see: ``iteredges``, ``select`` + """ + return self._edges[:] + + def iteredges(self): + """ + Return an iterator over the edges in this chart. It is + not guaranteed that new edges which are added to the + chart before the iterator is exhausted will also be generated. + + :rtype: iter(EdgeI) + :see: ``edges``, ``select`` + """ + return iter(self._edges) + + # Iterating over the chart yields its edges. + __iter__ = iteredges + + def num_edges(self): + """ + Return the number of edges contained in this chart. + + :rtype: int + """ + return len(self._edge_to_cpls) + + def select(self, **restrictions): + """ + Return an iterator over the edges in this chart. Any + new edges that are added to the chart before the iterator + is exahusted will also be generated. ``restrictions`` + can be used to restrict the set of edges that will be + generated. + + :param span: Only generate edges ``e`` where ``e.span()==span`` + :param start: Only generate edges ``e`` where ``e.start()==start`` + :param end: Only generate edges ``e`` where ``e.end()==end`` + :param length: Only generate edges ``e`` where ``e.length()==length`` + :param lhs: Only generate edges ``e`` where ``e.lhs()==lhs`` + :param rhs: Only generate edges ``e`` where ``e.rhs()==rhs`` + :param nextsym: Only generate edges ``e`` where + ``e.nextsym()==nextsym`` + :param dot: Only generate edges ``e`` where ``e.dot()==dot`` + :param is_complete: Only generate edges ``e`` where + ``e.is_complete()==is_complete`` + :param is_incomplete: Only generate edges ``e`` where + ``e.is_incomplete()==is_incomplete`` + :rtype: iter(EdgeI) + """ + # If there are no restrictions, then return all edges. + if restrictions == {}: + return iter(self._edges) + + # Find the index corresponding to the given restrictions. + restr_keys = sorted(restrictions.keys()) + restr_keys = tuple(restr_keys) + + # If it doesn't exist, then create it. + if restr_keys not in self._indexes: + self._add_index(restr_keys) + + vals = tuple(restrictions[key] for key in restr_keys) + return iter(self._indexes[restr_keys].get(vals, [])) + + def _add_index(self, restr_keys): + """ + A helper function for ``select``, which creates a new index for + a given set of attributes (aka restriction keys). + """ + # Make sure it's a valid index. + for key in restr_keys: + if not hasattr(EdgeI, key): + raise ValueError("Bad restriction: %s" % key) + + # Create the index. + index = self._indexes[restr_keys] = {} + + # Add all existing edges to the index. + for edge in self._edges: + vals = tuple(getattr(edge, key)() for key in restr_keys) + index.setdefault(vals, []).append(edge) + + def _register_with_indexes(self, edge): + """ + A helper function for ``insert``, which registers the new + edge with all existing indexes. + """ + for restr_keys, index in self._indexes.items(): + vals = tuple(getattr(edge, key)() for key in restr_keys) + index.setdefault(vals, []).append(edge) + + # //////////////////////////////////////////////////////////// + # Edge Insertion + # //////////////////////////////////////////////////////////// + + def insert_with_backpointer(self, new_edge, previous_edge, child_edge): + """ + Add a new edge to the chart, using a pointer to the previous edge. + """ + cpls = self.child_pointer_lists(previous_edge) + new_cpls = [cpl + (child_edge,) for cpl in cpls] + return self.insert(new_edge, *new_cpls) + + def insert(self, edge, *child_pointer_lists): + """ + Add a new edge to the chart, and return True if this operation + modified the chart. In particular, return true iff the chart + did not already contain ``edge``, or if it did not already associate + ``child_pointer_lists`` with ``edge``. + + :type edge: EdgeI + :param edge: The new edge + :type child_pointer_lists: sequence of tuple(EdgeI) + :param child_pointer_lists: A sequence of lists of the edges that + were used to form this edge. This list is used to reconstruct + the trees (or partial trees) that are associated with ``edge``. + :rtype: bool + """ + # Is it a new edge? + if edge not in self._edge_to_cpls: + # Add it to the list of edges. + self._append_edge(edge) + # Register with indexes. + self._register_with_indexes(edge) + + # Get the set of child pointer lists for this edge. + cpls = self._edge_to_cpls.setdefault(edge, OrderedDict()) + chart_was_modified = False + for child_pointer_list in child_pointer_lists: + child_pointer_list = tuple(child_pointer_list) + if child_pointer_list not in cpls: + # It's a new CPL; register it, and return true. + cpls[child_pointer_list] = True + chart_was_modified = True + return chart_was_modified + + def _append_edge(self, edge): + self._edges.append(edge) + + # //////////////////////////////////////////////////////////// + # Tree extraction & child pointer lists + # //////////////////////////////////////////////////////////// + + def parses(self, root, tree_class=Tree): + """ + Return an iterator of the complete tree structures that span + the entire chart, and whose root node is ``root``. + """ + for edge in self.select(start=0, end=self._num_leaves, lhs=root): + yield from self.trees(edge, tree_class=tree_class, complete=True) + + def trees(self, edge, tree_class=Tree, complete=False): + """ + Return an iterator of the tree structures that are associated + with ``edge``. + + If ``edge`` is incomplete, then the unexpanded children will be + encoded as childless subtrees, whose node value is the + corresponding terminal or nonterminal. + + :rtype: list(Tree) + :note: If two trees share a common subtree, then the same + Tree may be used to encode that subtree in + both trees. If you need to eliminate this subtree + sharing, then create a deep copy of each tree. + :raise ValueError: if more than ``MAX_PARSE_TREES`` parse-tree nodes + would be built, which a highly-ambiguous grammar reaches at an + exponential rate (CWE-770). + """ + budget = _ParseTreeBudget(MAX_PARSE_TREES) + return iter( + self._trees(edge, complete, memo={}, tree_class=tree_class, budget=budget) + ) + + def _trees(self, edge, complete, memo, tree_class, budget=None): + """ + A helper function for ``trees``. + + :param memo: A dictionary used to record the trees that we've + generated for each edge, so that when we see an edge more + than once, we can reuse the same trees. + :param budget: A shared :class:`_ParseTreeBudget` that bounds the total + number of parse-tree nodes built, so an exponential parse forest + cannot exhaust time/memory (CWE-770). + """ + if budget is None: + budget = _ParseTreeBudget(MAX_PARSE_TREES) + # If we've seen this edge before, then reuse our old answer. + if edge in memo: + return memo[edge] + + # when we're reading trees off the chart, don't use incomplete edges + if complete and edge.is_incomplete(): + return [] + + # Leaf edges. + if isinstance(edge, LeafEdge): + leaf = self._tokens[edge.start()] + memo[edge] = [leaf] + return [leaf] + + # Until we're done computing the trees for edge, set + # memo[edge] to be empty. This has the effect of filtering + # out any cyclic trees (i.e., trees that contain themselves as + # descendants), because if we reach this edge via a cycle, + # then it will appear that the edge doesn't generate any trees. + memo[edge] = [] + trees = [] + lhs = edge.lhs().symbol() + + # Each child pointer list can be used to form trees. + for cpl in self.child_pointer_lists(edge): + # Get the set of child choices for each child pointer. + # child_choices[i] is the set of choices for the tree's + # ith child. + child_choices = [ + self._trees(cp, complete, memo, tree_class, budget) for cp in cpl + ] + + # For each combination of children, add a tree. + for children in itertools.product(*child_choices): + # Count every node built; a highly-ambiguous grammar would + # otherwise materialise an exponential number of them (CWE-770). + budget.spend() + trees.append(tree_class(lhs, children)) + + # If the edge is incomplete, then extend it with "partial trees": + if edge.is_incomplete(): + unexpanded = [] + for elt in edge.rhs()[edge.dot() :]: + # Count these childless nodes too, so MAX_PARSE_TREES is an + # accurate bound on constructed tree nodes when trees() is used + # on incomplete edges (complete=False). + budget.spend() + unexpanded.append(tree_class(elt, [])) + for tree in trees: + tree.extend(unexpanded) + + # Update the memoization dictionary. + memo[edge] = trees + + # Return the list of trees. + return trees + + def child_pointer_lists(self, edge): + """ + Return the set of child pointer lists for the given edge. + Each child pointer list is a list of edges that have + been used to form this edge. + + :rtype: list(list(EdgeI)) + """ + # Make a copy, in case they modify it. + return self._edge_to_cpls.get(edge, {}).keys() + + # //////////////////////////////////////////////////////////// + # Display + # //////////////////////////////////////////////////////////// + def pretty_format_edge(self, edge, width=None): + """ + Return a pretty-printed string representation of a given edge + in this chart. + + :rtype: str + :param width: The number of characters allotted to each + index in the sentence. + """ + if width is None: + width = 50 // (self.num_leaves() + 1) + (start, end) = (edge.start(), edge.end()) + + str = "|" + ("." + " " * (width - 1)) * start + + # Zero-width edges are "#" if complete, ">" if incomplete + if start == end: + if edge.is_complete(): + str += "#" + else: + str += ">" + + # Spanning complete edges are "[===]"; Other edges are + # "[---]" if complete, "[--->" if incomplete + elif edge.is_complete() and edge.span() == (0, self._num_leaves): + str += "[" + ("=" * width) * (end - start - 1) + "=" * (width - 1) + "]" + elif edge.is_complete(): + str += "[" + ("-" * width) * (end - start - 1) + "-" * (width - 1) + "]" + else: + str += "[" + ("-" * width) * (end - start - 1) + "-" * (width - 1) + ">" + + str += (" " * (width - 1) + ".") * (self._num_leaves - end) + return str + "| %s" % edge + + def pretty_format_leaves(self, width=None): + """ + Return a pretty-printed string representation of this + chart's leaves. This string can be used as a header + for calls to ``pretty_format_edge``. + """ + if width is None: + width = 50 // (self.num_leaves() + 1) + + if self._tokens is not None and width > 1: + header = "|." + for tok in self._tokens: + header += tok[: width - 1].center(width - 1) + "." + header += "|" + else: + header = "" + + return header + + def pretty_format(self, width=None): + """ + Return a pretty-printed string representation of this chart. + + :param width: The number of characters allotted to each + index in the sentence. + :rtype: str + """ + if width is None: + width = 50 // (self.num_leaves() + 1) + # sort edges: primary key=length, secondary key=start index. + # (and filter out the token edges) + edges = sorted((e.length(), e.start(), e) for e in self) + edges = [e for (_, _, e) in edges] + + return ( + self.pretty_format_leaves(width) + + "\n" + + "\n".join(self.pretty_format_edge(edge, width) for edge in edges) + ) + + # //////////////////////////////////////////////////////////// + # Display: Dot (AT&T Graphviz) + # //////////////////////////////////////////////////////////// + + def dot_digraph(self): + # Header + s = "digraph nltk_chart {\n" + # s += ' size="5,5";\n' + s += " rankdir=LR;\n" + s += " node [height=0.1,width=0.1];\n" + s += ' node [style=filled, color="lightgray"];\n' + + # Set up the nodes + for y in range(self.num_edges(), -1, -1): + if y == 0: + s += ' node [style=filled, color="black"];\n' + for x in range(self.num_leaves() + 1): + if y == 0 or ( + x <= self._edges[y - 1].start() or x >= self._edges[y - 1].end() + ): + s += ' %04d.%04d [label=""];\n' % (x, y) + + # Add a spacer + s += " x [style=invis]; x->0000.0000 [style=invis];\n" + + # Declare ranks. + for x in range(self.num_leaves() + 1): + s += " {rank=same;" + for y in range(self.num_edges() + 1): + if y == 0 or ( + x <= self._edges[y - 1].start() or x >= self._edges[y - 1].end() + ): + s += " %04d.%04d" % (x, y) + s += "}\n" + + # Add the leaves + s += " edge [style=invis, weight=100];\n" + s += " node [shape=plaintext]\n" + s += " 0000.0000" + for x in range(self.num_leaves()): + s += "->%s->%04d.0000" % (self.leaf(x), x + 1) + s += ";\n\n" + + # Add the edges + s += " edge [style=solid, weight=1];\n" + for y, edge in enumerate(self): + for x in range(edge.start()): + s += ' %04d.%04d -> %04d.%04d [style="invis"];\n' % ( + x, + y + 1, + x + 1, + y + 1, + ) + s += ' %04d.%04d -> %04d.%04d [label="%s"];\n' % ( + edge.start(), + y + 1, + edge.end(), + y + 1, + edge, + ) + for x in range(edge.end(), self.num_leaves()): + s += ' %04d.%04d -> %04d.%04d [style="invis"];\n' % ( + x, + y + 1, + x + 1, + y + 1, + ) + s += "}\n" + return s + + +######################################################################## +## Chart Rules +######################################################################## + + +class ChartRuleI: + """ + A rule that specifies what new edges are licensed by any given set + of existing edges. Each chart rule expects a fixed number of + edges, as indicated by the class variable ``NUM_EDGES``. In + particular: + + - A chart rule with ``NUM_EDGES=0`` specifies what new edges are + licensed, regardless of existing edges. + - A chart rule with ``NUM_EDGES=1`` specifies what new edges are + licensed by a single existing edge. + - A chart rule with ``NUM_EDGES=2`` specifies what new edges are + licensed by a pair of existing edges. + + :type NUM_EDGES: int + :cvar NUM_EDGES: The number of existing edges that this rule uses + to license new edges. Typically, this number ranges from zero + to two. + """ + + def apply(self, chart, grammar, *edges): + """ + Return a generator that will add edges licensed by this rule + and the given edges to the chart, one at a time. Each + time the generator is resumed, it will either add a new + edge and yield that edge; or return. + + :type edges: list(EdgeI) + :param edges: A set of existing edges. The number of edges + that should be passed to ``apply()`` is specified by the + ``NUM_EDGES`` class variable. + :rtype: iter(EdgeI) + """ + raise NotImplementedError() + + def apply_everywhere(self, chart, grammar): + """ + Return a generator that will add all edges licensed by + this rule, given the edges that are currently in the + chart, one at a time. Each time the generator is resumed, + it will either add a new edge and yield that edge; or return. + + :rtype: iter(EdgeI) + """ + raise NotImplementedError() + + +class AbstractChartRule(ChartRuleI): + """ + An abstract base class for chart rules. ``AbstractChartRule`` + provides: + + - A default implementation for ``apply``. + - A default implementation for ``apply_everywhere``, + (Currently, this implementation assumes that ``NUM_EDGES <= 3``.) + - A default implementation for ``__str__``, which returns a + name based on the rule's class name. + """ + + # Subclasses must define apply. + def apply(self, chart, grammar, *edges): + raise NotImplementedError() + + # Default: loop through the given number of edges, and call + # self.apply() for each set of edges. + def apply_everywhere(self, chart, grammar): + if self.NUM_EDGES == 0: + yield from self.apply(chart, grammar) + + elif self.NUM_EDGES == 1: + for e1 in chart: + yield from self.apply(chart, grammar, e1) + + elif self.NUM_EDGES == 2: + for e1 in chart: + for e2 in chart: + yield from self.apply(chart, grammar, e1, e2) + + elif self.NUM_EDGES == 3: + for e1 in chart: + for e2 in chart: + for e3 in chart: + yield from self.apply(chart, grammar, e1, e2, e3) + + else: + raise AssertionError("NUM_EDGES>3 is not currently supported") + + # Default: return a name based on the class name. + def __str__(self): + # Add spaces between InitialCapsWords. + return re.sub("([a-z])([A-Z])", r"\1 \2", self.__class__.__name__) + + +# //////////////////////////////////////////////////////////// +# Fundamental Rule +# //////////////////////////////////////////////////////////// + + +class FundamentalRule(AbstractChartRule): + r""" + A rule that joins two adjacent edges to form a single combined + edge. In particular, this rule specifies that any pair of edges + + - ``[A -> alpha \* B beta][i:j]`` + - ``[B -> gamma \*][j:k]`` + + licenses the edge: + + - ``[A -> alpha B * beta][i:j]`` + """ + + NUM_EDGES = 2 + + def apply(self, chart, grammar, left_edge, right_edge): + # Make sure the rule is applicable. + if not ( + left_edge.is_incomplete() + and right_edge.is_complete() + and left_edge.end() == right_edge.start() + and left_edge.nextsym() == right_edge.lhs() + ): + return + + # Construct the new edge. + new_edge = left_edge.move_dot_forward(right_edge.end()) + + # Insert it into the chart. + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + +class SingleEdgeFundamentalRule(FundamentalRule): + r""" + A rule that joins a given edge with adjacent edges in the chart, + to form combined edges. In particular, this rule specifies that + either of the edges: + + - ``[A -> alpha \* B beta][i:j]`` + - ``[B -> gamma \*][j:k]`` + + licenses the edge: + + - ``[A -> alpha B * beta][i:j]`` + + if the other edge is already in the chart. + + :note: This is basically ``FundamentalRule``, with one edge left + unspecified. + """ + + NUM_EDGES = 1 + + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + yield from self._apply_incomplete(chart, grammar, edge) + else: + yield from self._apply_complete(chart, grammar, edge) + + def _apply_complete(self, chart, grammar, right_edge): + for left_edge in chart.select( + end=right_edge.start(), is_complete=False, nextsym=right_edge.lhs() + ): + new_edge = left_edge.move_dot_forward(right_edge.end()) + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + def _apply_incomplete(self, chart, grammar, left_edge): + for right_edge in chart.select( + start=left_edge.end(), is_complete=True, lhs=left_edge.nextsym() + ): + new_edge = left_edge.move_dot_forward(right_edge.end()) + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + +# //////////////////////////////////////////////////////////// +# Inserting Terminal Leafs +# //////////////////////////////////////////////////////////// + + +class LeafInitRule(AbstractChartRule): + NUM_EDGES = 0 + + def apply(self, chart, grammar): + for index in range(chart.num_leaves()): + new_edge = LeafEdge(chart.leaf(index), index) + if chart.insert(new_edge, ()): + yield new_edge + + +# //////////////////////////////////////////////////////////// +# Top-Down Prediction +# //////////////////////////////////////////////////////////// + + +class TopDownInitRule(AbstractChartRule): + r""" + A rule licensing edges corresponding to the grammar productions for + the grammar's start symbol. In particular, this rule specifies that + ``[S -> \* alpha][0:i]`` is licensed for each grammar production + ``S -> alpha``, where ``S`` is the grammar's start symbol. + """ + + NUM_EDGES = 0 + + def apply(self, chart, grammar): + for prod in grammar.productions(lhs=grammar.start()): + new_edge = TreeEdge.from_production(prod, 0) + if chart.insert(new_edge, ()): + yield new_edge + + +class TopDownPredictRule(AbstractChartRule): + r""" + A rule licensing edges corresponding to the grammar productions + for the nonterminal following an incomplete edge's dot. In + particular, this rule specifies that + ``[A -> alpha \* B beta][i:j]`` licenses the edge + ``[B -> \* gamma][j:j]`` for each grammar production ``B -> gamma``. + + :note: This rule corresponds to the Predictor Rule in Earley parsing. + """ + + NUM_EDGES = 1 + + def apply(self, chart, grammar, edge): + if edge.is_complete(): + return + for prod in grammar.productions(lhs=edge.nextsym()): + new_edge = TreeEdge.from_production(prod, edge.end()) + if chart.insert(new_edge, ()): + yield new_edge + + +class CachedTopDownPredictRule(TopDownPredictRule): + r""" + A cached version of ``TopDownPredictRule``. After the first time + this rule is applied to an edge with a given ``end`` and ``next``, + it will not generate any more edges for edges with that ``end`` and + ``next``. + + If ``chart`` or ``grammar`` are changed, then the cache is flushed. + """ + + def __init__(self): + TopDownPredictRule.__init__(self) + self._done = {} + + def apply(self, chart, grammar, edge): + if edge.is_complete(): + return + nextsym, index = edge.nextsym(), edge.end() + if not is_nonterminal(nextsym): + return + + # If we've already applied this rule to an edge with the same + # next & end, and the chart & grammar have not changed, then + # just return (no new edges to add). + done = self._done.get((nextsym, index), (None, None)) + if done[0] is chart and done[1] is grammar: + return + + # Add all the edges indicated by the top down expand rule. + for prod in grammar.productions(lhs=nextsym): + # If the left corner in the predicted production is + # leaf, it must match with the input. + if prod.rhs(): + first = prod.rhs()[0] + if is_terminal(first): + if index >= chart.num_leaves() or first != chart.leaf(index): + continue + + new_edge = TreeEdge.from_production(prod, index) + if chart.insert(new_edge, ()): + yield new_edge + + # Record the fact that we've applied this rule. + self._done[nextsym, index] = (chart, grammar) + + +# //////////////////////////////////////////////////////////// +# Bottom-Up Prediction +# //////////////////////////////////////////////////////////// + + +class BottomUpPredictRule(AbstractChartRule): + r""" + A rule licensing any edge corresponding to a production whose + right-hand side begins with a complete edge's left-hand side. In + particular, this rule specifies that ``[A -> alpha \*]`` licenses + the edge ``[B -> \* A beta]`` for each grammar production ``B -> A beta``. + """ + + NUM_EDGES = 1 + + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + return + for prod in grammar.productions(rhs=edge.lhs()): + new_edge = TreeEdge.from_production(prod, edge.start()) + if chart.insert(new_edge, ()): + yield new_edge + + +class BottomUpPredictCombineRule(BottomUpPredictRule): + r""" + A rule licensing any edge corresponding to a production whose + right-hand side begins with a complete edge's left-hand side. In + particular, this rule specifies that ``[A -> alpha \*]`` + licenses the edge ``[B -> A \* beta]`` for each grammar + production ``B -> A beta``. + + :note: This is like ``BottomUpPredictRule``, but it also applies + the ``FundamentalRule`` to the resulting edge. + """ + + NUM_EDGES = 1 + + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + return + for prod in grammar.productions(rhs=edge.lhs()): + new_edge = TreeEdge(edge.span(), prod.lhs(), prod.rhs(), 1) + if chart.insert(new_edge, (edge,)): + yield new_edge + + +class EmptyPredictRule(AbstractChartRule): + """ + A rule that inserts all empty productions as passive edges, + in every position in the chart. + """ + + NUM_EDGES = 0 + + def apply(self, chart, grammar): + for prod in grammar.productions(empty=True): + for index in range(chart.num_leaves() + 1): + new_edge = TreeEdge.from_production(prod, index) + if chart.insert(new_edge, ()): + yield new_edge + + +######################################################################## +## Filtered Bottom Up +######################################################################## + + +class FilteredSingleEdgeFundamentalRule(SingleEdgeFundamentalRule): + def _apply_complete(self, chart, grammar, right_edge): + end = right_edge.end() + nexttoken = end < chart.num_leaves() and chart.leaf(end) + for left_edge in chart.select( + end=right_edge.start(), is_complete=False, nextsym=right_edge.lhs() + ): + if _bottomup_filter(grammar, nexttoken, left_edge.rhs(), left_edge.dot()): + new_edge = left_edge.move_dot_forward(right_edge.end()) + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + def _apply_incomplete(self, chart, grammar, left_edge): + for right_edge in chart.select( + start=left_edge.end(), is_complete=True, lhs=left_edge.nextsym() + ): + end = right_edge.end() + nexttoken = end < chart.num_leaves() and chart.leaf(end) + if _bottomup_filter(grammar, nexttoken, left_edge.rhs(), left_edge.dot()): + new_edge = left_edge.move_dot_forward(right_edge.end()) + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + +class FilteredBottomUpPredictCombineRule(BottomUpPredictCombineRule): + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + return + + end = edge.end() + nexttoken = end < chart.num_leaves() and chart.leaf(end) + for prod in grammar.productions(rhs=edge.lhs()): + if _bottomup_filter(grammar, nexttoken, prod.rhs()): + new_edge = TreeEdge(edge.span(), prod.lhs(), prod.rhs(), 1) + if chart.insert(new_edge, (edge,)): + yield new_edge + + +def _bottomup_filter(grammar, nexttoken, rhs, dot=0): + if len(rhs) <= dot + 1: + return True + _next = rhs[dot + 1] + if is_terminal(_next): + return nexttoken == _next + else: + return grammar.is_leftcorner(_next, nexttoken) + + +######################################################################## +## Generic Chart Parser +######################################################################## + +TD_STRATEGY = [ + LeafInitRule(), + TopDownInitRule(), + CachedTopDownPredictRule(), + SingleEdgeFundamentalRule(), +] +BU_STRATEGY = [ + LeafInitRule(), + EmptyPredictRule(), + BottomUpPredictRule(), + SingleEdgeFundamentalRule(), +] +BU_LC_STRATEGY = [ + LeafInitRule(), + EmptyPredictRule(), + BottomUpPredictCombineRule(), + SingleEdgeFundamentalRule(), +] + +LC_STRATEGY = [ + LeafInitRule(), + FilteredBottomUpPredictCombineRule(), + FilteredSingleEdgeFundamentalRule(), +] + + +class ChartParser(ParserI): + """ + A generic chart parser. A "strategy", or list of + ``ChartRuleI`` instances, is used to decide what edges to add to + the chart. In particular, ``ChartParser`` uses the following + algorithm to parse texts: + + | Until no new edges are added: + | For each *rule* in *strategy*: + | Apply *rule* to any applicable edges in the chart. + | Return any complete parses in the chart + """ + + def __init__( + self, + grammar, + strategy=BU_LC_STRATEGY, + trace=0, + trace_chart_width=50, + use_agenda=True, + chart_class=Chart, + ): + """ + Create a new chart parser, that uses ``grammar`` to parse + texts. + + :type grammar: CFG + :param grammar: The grammar used to parse texts. + :type strategy: list(ChartRuleI) + :param strategy: A list of rules that should be used to decide + what edges to add to the chart (top-down strategy by default). + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing + output. + :type trace_chart_width: int + :param trace_chart_width: The default total width reserved for + the chart in trace output. The remainder of each line will + be used to display edges. + :type use_agenda: bool + :param use_agenda: Use an optimized agenda-based algorithm, + if possible. + :param chart_class: The class that should be used to create + the parse charts. + """ + self._grammar = grammar + self._strategy = strategy + self._trace = trace + self._trace_chart_width = trace_chart_width + # If the strategy only consists of axioms (NUM_EDGES==0) and + # inference rules (NUM_EDGES==1), we can use an agenda-based algorithm: + self._use_agenda = use_agenda + self._chart_class = chart_class + + self._axioms = [] + self._inference_rules = [] + for rule in strategy: + if rule.NUM_EDGES == 0: + self._axioms.append(rule) + elif rule.NUM_EDGES == 1: + self._inference_rules.append(rule) + else: + self._use_agenda = False + + def grammar(self): + return self._grammar + + def _trace_new_edges(self, chart, rule, new_edges, trace, edge_width): + if not trace: + return + print_rule_header = trace > 1 + for edge in new_edges: + if print_rule_header: + print("%s:" % rule) + print_rule_header = False + print(chart.pretty_format_edge(edge, edge_width)) + + def chart_parse(self, tokens, trace=None): + """ + Return the final parse ``Chart`` from which all possible + parse trees can be extracted. + + :param tokens: The sentence to be parsed + :type tokens: list(str) + :rtype: Chart + """ + if trace is None: + trace = self._trace + trace_new_edges = self._trace_new_edges + + tokens = list(tokens) + self._grammar.check_coverage(tokens) + chart = self._chart_class(tokens) + grammar = self._grammar + + # Width, for printing trace edges. + trace_edge_width = self._trace_chart_width // (chart.num_leaves() + 1) + if trace: + print(chart.pretty_format_leaves(trace_edge_width)) + + if self._use_agenda: + # Use an agenda-based algorithm. + for axiom in self._axioms: + new_edges = list(axiom.apply(chart, grammar)) + trace_new_edges(chart, axiom, new_edges, trace, trace_edge_width) + + inference_rules = self._inference_rules + agenda = chart.edges() + # We reverse the initial agenda, since it is a stack + # but chart.edges() functions as a queue. + agenda.reverse() + while agenda: + edge = agenda.pop() + for rule in inference_rules: + new_edges = list(rule.apply(chart, grammar, edge)) + if trace: + trace_new_edges(chart, rule, new_edges, trace, trace_edge_width) + agenda += new_edges + + else: + # Do not use an agenda-based algorithm. + edges_added = True + while edges_added: + edges_added = False + for rule in self._strategy: + new_edges = list(rule.apply_everywhere(chart, grammar)) + edges_added = len(new_edges) + trace_new_edges(chart, rule, new_edges, trace, trace_edge_width) + + # Return the final chart. + return chart + + def parse(self, tokens, tree_class=Tree): + chart = self.chart_parse(tokens) + return iter(chart.parses(self._grammar.start(), tree_class=tree_class)) + + +class TopDownChartParser(ChartParser): + """ + A ``ChartParser`` using a top-down parsing strategy. + See ``ChartParser`` for more information. + """ + + def __init__(self, grammar, **parser_args): + ChartParser.__init__(self, grammar, TD_STRATEGY, **parser_args) + + +class BottomUpChartParser(ChartParser): + """ + A ``ChartParser`` using a bottom-up parsing strategy. + See ``ChartParser`` for more information. + """ + + def __init__(self, grammar, **parser_args): + if isinstance(grammar, PCFG): + warnings.warn( + "BottomUpChartParser only works for CFG, " + "use BottomUpProbabilisticChartParser instead", + category=DeprecationWarning, + ) + ChartParser.__init__(self, grammar, BU_STRATEGY, **parser_args) + + +class BottomUpLeftCornerChartParser(ChartParser): + """ + A ``ChartParser`` using a bottom-up left-corner parsing strategy. + This strategy is often more efficient than standard bottom-up. + See ``ChartParser`` for more information. + """ + + def __init__(self, grammar, **parser_args): + ChartParser.__init__(self, grammar, BU_LC_STRATEGY, **parser_args) + + +class LeftCornerChartParser(ChartParser): + def __init__(self, grammar, **parser_args): + if not grammar.is_nonempty(): + raise ValueError( + "LeftCornerParser only works for grammars " "without empty productions." + ) + ChartParser.__init__(self, grammar, LC_STRATEGY, **parser_args) + + +######################################################################## +## Stepping Chart Parser +######################################################################## + + +class SteppingChartParser(ChartParser): + """ + A ``ChartParser`` that allows you to step through the parsing + process, adding a single edge at a time. It also allows you to + change the parser's strategy or grammar midway through parsing a + text. + + The ``initialize`` method is used to start parsing a text. ``step`` + adds a single edge to the chart. ``set_strategy`` changes the + strategy used by the chart parser. ``parses`` returns the set of + parses that has been found by the chart parser. + + :ivar _restart: Records whether the parser's strategy, grammar, + or chart has been changed. If so, then ``step`` must restart + the parsing algorithm. + """ + + def __init__(self, grammar, strategy=[], trace=0): + self._chart = None + self._current_chartrule = None + self._restart = False + ChartParser.__init__(self, grammar, strategy, trace) + + # //////////////////////////////////////////////////////////// + # Initialization + # //////////////////////////////////////////////////////////// + + def initialize(self, tokens): + "Begin parsing the given tokens." + self._chart = Chart(list(tokens)) + self._restart = True + + # //////////////////////////////////////////////////////////// + # Stepping + # //////////////////////////////////////////////////////////// + + def step(self): + """ + Return a generator that adds edges to the chart, one at a + time. Each time the generator is resumed, it adds a single + edge and yields that edge. If no more edges can be added, + then it yields None. + + If the parser's strategy, grammar, or chart is changed, then + the generator will continue adding edges using the new + strategy, grammar, or chart. + + Note that this generator never terminates, since the grammar + or strategy might be changed to values that would add new + edges. Instead, it yields None when no more edges can be + added with the current strategy and grammar. + """ + if self._chart is None: + raise ValueError("Parser must be initialized first") + while True: + self._restart = False + w = 50 // (self._chart.num_leaves() + 1) + + for e in self._parse(): + if self._trace > 1: + print(self._current_chartrule) + if self._trace > 0: + print(self._chart.pretty_format_edge(e, w)) + yield e + if self._restart: + break + else: + yield None # No more edges. + + def _parse(self): + """ + A generator that implements the actual parsing algorithm. + ``step`` iterates through this generator, and restarts it + whenever the parser's strategy, grammar, or chart is modified. + """ + chart = self._chart + grammar = self._grammar + edges_added = 1 + while edges_added > 0: + edges_added = 0 + for rule in self._strategy: + self._current_chartrule = rule + for e in rule.apply_everywhere(chart, grammar): + edges_added += 1 + yield e + + # //////////////////////////////////////////////////////////// + # Accessors + # //////////////////////////////////////////////////////////// + + def strategy(self): + "Return the strategy used by this parser." + return self._strategy + + def grammar(self): + "Return the grammar used by this parser." + return self._grammar + + def chart(self): + "Return the chart that is used by this parser." + return self._chart + + def current_chartrule(self): + "Return the chart rule used to generate the most recent edge." + return self._current_chartrule + + def parses(self, tree_class=Tree): + "Return the parse trees currently contained in the chart." + return self._chart.parses(self._grammar.start(), tree_class) + + # //////////////////////////////////////////////////////////// + # Parser modification + # //////////////////////////////////////////////////////////// + + def set_strategy(self, strategy): + """ + Change the strategy that the parser uses to decide which edges + to add to the chart. + + :type strategy: list(ChartRuleI) + :param strategy: A list of rules that should be used to decide + what edges to add to the chart. + """ + if strategy == self._strategy: + return + self._strategy = strategy[:] # Make a copy. + self._restart = True + + def set_grammar(self, grammar): + "Change the grammar used by the parser." + if grammar is self._grammar: + return + self._grammar = grammar + self._restart = True + + def set_chart(self, chart): + "Load a given chart into the chart parser." + if chart is self._chart: + return + self._chart = chart + self._restart = True + + # //////////////////////////////////////////////////////////// + # Standard parser methods + # //////////////////////////////////////////////////////////// + + def parse(self, tokens, tree_class=Tree): + tokens = list(tokens) + self._grammar.check_coverage(tokens) + + # Initialize ourselves. + self.initialize(tokens) + + # Step until no more edges are generated. + for e in self.step(): + if e is None: + break + + # Return an iterator of complete parses. + return self.parses(tree_class=tree_class) + + +######################################################################## +## Demo Code +######################################################################## + + +def demo_grammar(): + from nltk.grammar import CFG + + return CFG.fromstring( + """ +S -> NP VP +PP -> "with" NP +NP -> NP PP +VP -> VP PP +VP -> Verb NP +VP -> Verb +NP -> Det Noun +NP -> "John" +NP -> "I" +Det -> "the" +Det -> "my" +Det -> "a" +Noun -> "dog" +Noun -> "cookie" +Verb -> "ate" +Verb -> "saw" +Prep -> "with" +Prep -> "under" +""" + ) + + +def demo( + choice=None, + print_times=True, + print_grammar=False, + print_trees=True, + trace=2, + sent="I saw John with a dog with my cookie", + numparses=5, +): + """ + A demonstration of the chart parsers. + """ + import sys + import time + + from nltk import CFG, Production, nonterminals + + # The grammar for ChartParser and SteppingChartParser: + grammar = demo_grammar() + if print_grammar: + print("* Grammar") + print(grammar) + + # Tokenize the sample sentence. + print("* Sentence:") + print(sent) + tokens = sent.split() + print(tokens) + print() + + # Ask the user which parser to test, + # if the parser wasn't provided as an argument + if choice is None: + print(" 1: Top-down chart parser") + print(" 2: Bottom-up chart parser") + print(" 3: Bottom-up left-corner chart parser") + print(" 4: Left-corner chart parser with bottom-up filter") + print(" 5: Stepping chart parser (alternating top-down & bottom-up)") + print(" 6: All parsers") + print("\nWhich parser (1-6)? ", end=" ") + choice = sys.stdin.readline().strip() + print() + + choice = str(choice) + if choice not in "123456": + print("Bad parser number") + return + + # Keep track of how long each parser takes. + times = {} + + strategies = { + "1": ("Top-down", TD_STRATEGY), + "2": ("Bottom-up", BU_STRATEGY), + "3": ("Bottom-up left-corner", BU_LC_STRATEGY), + "4": ("Filtered left-corner", LC_STRATEGY), + } + choices = [] + if choice in strategies: + choices = [choice] + if choice == "6": + choices = "1234" + + # Run the requested chart parser(s), except the stepping parser. + for strategy in choices: + print("* Strategy: " + strategies[strategy][0]) + print() + cp = ChartParser(grammar, strategies[strategy][1], trace=trace) + t = time.time() + chart = cp.chart_parse(tokens) + parses = list(chart.parses(grammar.start())) + + times[strategies[strategy][0]] = time.time() - t + print("Nr edges in chart:", len(chart.edges())) + if numparses: + assert len(parses) == numparses, "Not all parses found" + if print_trees: + for tree in parses: + print(tree) + else: + print("Nr trees:", len(parses)) + print() + + # Run the stepping parser, if requested. + if choice in "56": + print("* Strategy: Stepping (top-down vs bottom-up)") + print() + t = time.time() + cp = SteppingChartParser(grammar, trace=trace) + cp.initialize(tokens) + for i in range(5): + print("*** SWITCH TO TOP DOWN") + cp.set_strategy(TD_STRATEGY) + for j, e in enumerate(cp.step()): + if j > 20 or e is None: + break + print("*** SWITCH TO BOTTOM UP") + cp.set_strategy(BU_STRATEGY) + for j, e in enumerate(cp.step()): + if j > 20 or e is None: + break + times["Stepping"] = time.time() - t + print("Nr edges in chart:", len(cp.chart().edges())) + if numparses: + assert len(list(cp.parses())) == numparses, "Not all parses found" + if print_trees: + for tree in cp.parses(): + print(tree) + else: + print("Nr trees:", len(list(cp.parses()))) + print() + + # Print the times of all parsers: + if not (print_times and times): + return + print("* Parsing times") + print() + maxlen = max(len(key) for key in times) + format = "%" + repr(maxlen) + "s parser: %6.3fsec" + times_items = times.items() + for parser, t in sorted(times_items, key=lambda a: a[1]): + print(format % (parser, t)) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/corenlp.py b/nltk/parse/corenlp.py new file mode 100644 index 0000000..98d234f --- /dev/null +++ b/nltk/parse/corenlp.py @@ -0,0 +1,824 @@ +# Natural Language Toolkit: Interface to the CoreNLP REST API. +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Dmitrijs Milajevs +# +# URL: +# For license information, see LICENSE.TXT + +import json +import os +import re +import socket +import time +from typing import List, Tuple + +from nltk.internals import _java_options, config_java, find_jar_iter, java +from nltk.parse.api import ParserI +from nltk.parse.dependencygraph import DependencyGraph +from nltk.tag.api import TaggerI +from nltk.tokenize.api import TokenizerI +from nltk.tree import Tree + +_stanford_url = "https://stanfordnlp.github.io/CoreNLP/" + + +class CoreNLPServerError(EnvironmentError): + """Exceptions associated with the Core NLP server.""" + + +def try_port(port=0): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(("localhost", port)) + + p = sock.getsockname()[1] + sock.close() + + return p + + +class CoreNLPServer: + _MODEL_JAR_PATTERN = r"stanford-corenlp-(\d+)\.(\d+)\.(\d+)-models\.jar" + _JAR = r"stanford-corenlp-(\d+)\.(\d+)\.(\d+)\.jar" + + def __init__( + self, + path_to_jar=None, + path_to_models_jar=None, + verbose=False, + java_options=None, + corenlp_options=None, + port=None, + ): + if corenlp_options is None: + corenlp_options = ["-preload", "tokenize,ssplit,pos,lemma,parse,depparse"] + + jars = list( + find_jar_iter( + self._JAR, + path_to_jar, + env_vars=("CORENLP",), + searchpath=(), + url=_stanford_url, + verbose=verbose, + is_regex=True, + ) + ) + + # find the most recent code and model jar + stanford_jar = max(jars, key=lambda model_name: re.match(self._JAR, model_name)) + + if port is None: + try: + port = try_port(9000) + except OSError: + port = try_port() + corenlp_options.extend(["-port", str(port)]) + else: + try_port(port) + corenlp_options.extend(["-port", str(port)]) + + self.url = f"http://localhost:{port}" + + model_jar = max( + find_jar_iter( + self._MODEL_JAR_PATTERN, + path_to_models_jar, + env_vars=("CORENLP_MODELS",), + searchpath=(), + url=_stanford_url, + verbose=verbose, + is_regex=True, + ), + key=lambda model_name: re.match(self._MODEL_JAR_PATTERN, model_name), + ) + + self.verbose = verbose + + self._classpath = stanford_jar, model_jar + + self.corenlp_options = corenlp_options + self.java_options = java_options or ["-mx2g"] + + def start(self, stdout="devnull", stderr="devnull"): + """Start the CoreNLP server + + This method checks the status of the started server, but does **not** stop + the server process if those checks fail. If you want the server process + to be stopped in that case, either use this class as a context manager + (see :meth:`.__enter__()`) or catch :exc:`~corenlp.CoreNLPServerError` + exception and stop the server manually. + + :param stdout, stderr: Specifies where CoreNLP output is redirected. Valid values are 'devnull', 'stdout', 'pipe' + :raises CoreNLPServerError: If the server fails to start or a status check fails + (in which case the server process remains running). + """ + import requests + + cmd = ["edu.stanford.nlp.pipeline.StanfordCoreNLPServer"] + + if self.corenlp_options: + cmd.extend(self.corenlp_options) + + # Configure java. + default_options = " ".join(_java_options) + config_java(options=self.java_options, verbose=self.verbose) + + try: + self.popen = java( + cmd, + classpath=self._classpath, + blocking=False, + stdout=stdout, + stderr=stderr, + ) + finally: + # Return java configurations to their default values. + config_java(options=default_options, verbose=self.verbose) + + # Check that the server is istill running. + returncode = self.popen.poll() + if returncode is not None: + _, stderrdata = self.popen.communicate() + raise CoreNLPServerError( + returncode, + "Could not start the server. " + "The error was: {}".format(stderrdata.decode("ascii")), + ) + + for i in range(30): + try: + response = requests.get(requests.compat.urljoin(self.url, "live")) + except requests.exceptions.ConnectionError: + time.sleep(1) + else: + if response.ok: + break + else: + raise CoreNLPServerError("Could not connect to the server.") + + for i in range(60): + try: + response = requests.get(requests.compat.urljoin(self.url, "ready")) + except requests.exceptions.ConnectionError: + time.sleep(1) + else: + if response.ok: + break + else: + raise CoreNLPServerError("The server is not ready.") + + def stop(self): + self.popen.terminate() + self.popen.wait() + + def __enter__(self): + """Start the CoreNLP server + + This method checks the status of the started server and stops the server process + if those checks fail. If you want the server process to **not** be stopped in that case, + use the :meth:`.start`/ :meth:`.stop` methods. + + :raises CoreNLPServerError: If the server fails to start. + """ + try: + self.start() + except CoreNLPServerError: + self.stop() + raise + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.stop() + return False + + +class GenericCoreNLPParser(ParserI, TokenizerI, TaggerI): + """Interface to the CoreNLP Parser.""" + + def __init__( + self, + url="http://localhost:9000", + encoding="utf8", + tagtype=None, + strict_json=True, + ): + import requests + + self.url = url + self.encoding = encoding + + if tagtype not in ["pos", "ner", None]: + raise ValueError("tagtype must be either 'pos', 'ner' or None") + + self.tagtype = tagtype + self.strict_json = strict_json + + self.session = requests.Session() + + def parse_sents(self, sentences, *args, **kwargs): + """Parse multiple sentences. + + Takes multiple sentences as a list where each sentence is a list of + words. Each sentence will be automatically tagged with this + CoreNLPParser instance's tagger. + + If a whitespace exists inside a token, then the token will be treated as + several tokens. + + :param sentences: Input sentences to parse + :type sentences: list(list(str)) + :rtype: iter(iter(Tree)) + """ + # Converting list(list(str)) -> list(str) + sentences = (" ".join(words) for words in sentences) + return self.raw_parse_sents(sentences, *args, **kwargs) + + def raw_parse(self, sentence, properties=None, *args, **kwargs): + """Parse a sentence. + + Takes a sentence as a string; before parsing, it will be automatically + tokenized and tagged by the CoreNLP Parser. + + :param sentence: Input sentence to parse + :type sentence: str + :rtype: iter(Tree) + """ + default_properties = {"tokenize.whitespace": "false"} + default_properties.update(properties or {}) + + return next( + self.raw_parse_sents( + [sentence], properties=default_properties, *args, **kwargs + ) + ) + + def api_call(self, data, properties=None, timeout=60): + default_properties = { + "outputFormat": "json", + "annotators": "tokenize,pos,lemma,ssplit,{parser_annotator}".format( + parser_annotator=self.parser_annotator + ), + } + + default_properties.update(properties or {}) + + response = self.session.post( + self.url, + params={"properties": json.dumps(default_properties)}, + data=data.encode(self.encoding), + headers={"Content-Type": f"text/plain; charset={self.encoding}"}, + timeout=timeout, + ) + + response.raise_for_status() + + return response.json(strict=self.strict_json) + + def raw_parse_sents( + self, sentences, verbose=False, properties=None, *args, **kwargs + ): + """Parse multiple sentences. + + Takes multiple sentences as a list of strings. Each sentence will be + automatically tokenized and tagged. + + :param sentences: Input sentences to parse. + :type sentences: list(str) + :rtype: iter(iter(Tree)) + + """ + default_properties = { + # Only splits on '\n', never inside the sentence. + "ssplit.eolonly": "true" + } + + default_properties.update(properties or {}) + + """ + for sentence in sentences: + parsed_data = self.api_call(sentence, properties=default_properties) + + assert len(parsed_data['sentences']) == 1 + + for parse in parsed_data['sentences']: + tree = self.make_tree(parse) + yield iter([tree]) + """ + parsed_data = self.api_call("\n".join(sentences), properties=default_properties) + for parsed_sent in parsed_data["sentences"]: + tree = self.make_tree(parsed_sent) + yield iter([tree]) + + def parse_text(self, text, *args, **kwargs): + """Parse a piece of text. + + The text might contain several sentences which will be split by CoreNLP. + + :param str text: text to be split. + :returns: an iterable of syntactic structures. # TODO: should it be an iterable of iterables? + + """ + parsed_data = self.api_call(text, *args, **kwargs) + + for parse in parsed_data["sentences"]: + yield self.make_tree(parse) + + def tokenize(self, text, properties=None): + """Tokenize a string of text. + + Skip these tests if CoreNLP is likely not ready. + >>> from nltk.test.setup_fixt import check_jar + >>> check_jar(CoreNLPServer._JAR, env_vars=("CORENLP",), is_regex=True) + + The CoreNLP server can be started using the following notation, although + we recommend the `with CoreNLPServer() as server:` context manager notation + to ensure that the server is always stopped. + >>> server = CoreNLPServer() + >>> server.start() + >>> parser = CoreNLPParser(url=server.url) + + >>> text = 'Good muffins cost $3.88\\nin New York. Please buy me\\ntwo of them.\\nThanks.' + >>> list(parser.tokenize(text)) + ['Good', 'muffins', 'cost', '$', '3.88', 'in', 'New', 'York', '.', 'Please', 'buy', 'me', 'two', 'of', 'them', '.', 'Thanks', '.'] + + >>> s = "The colour of the wall is blue." + >>> list( + ... parser.tokenize( + ... 'The colour of the wall is blue.', + ... properties={'tokenize.options': 'americanize=true'}, + ... ) + ... ) + ['The', 'colour', 'of', 'the', 'wall', 'is', 'blue', '.'] + >>> server.stop() + + """ + default_properties = {"annotators": "tokenize,ssplit"} + + default_properties.update(properties or {}) + + result = self.api_call(text, properties=default_properties) + + for sentence in result["sentences"]: + for token in sentence["tokens"]: + yield token["originalText"] or token["word"] + + def tag_sents(self, sentences, properties=None): + """ + Tag multiple sentences. + + Takes multiple sentences as a list where each sentence is a list of + tokens. + + :param sentences: Input sentences to tag + :type sentences: list(list(str)) + :rtype: list(list(tuple(str, str)) + """ + + # Converting list(list(str)) -> list(str) + sentences = (" ".join(words) for words in sentences) + + if properties is None: + properties = {"tokenize.whitespace": "true", "ner.useSUTime": "false"} + + return [sentences[0] for sentences in self.raw_tag_sents(sentences, properties)] + + def tag(self, sentence: str, properties=None) -> list[tuple[str, str]]: + """ + Tag a list of tokens. + + :rtype: list(tuple(str, str)) + + Skip these tests if CoreNLP is likely not ready. + >>> from nltk.test.setup_fixt import check_jar + >>> check_jar(CoreNLPServer._JAR, env_vars=("CORENLP",), is_regex=True) + + The CoreNLP server can be started using the following notation, although + we recommend the `with CoreNLPServer() as server:` context manager notation + to ensure that the server is always stopped. + >>> server = CoreNLPServer() + >>> server.start() + >>> parser = CoreNLPParser(url=server.url, tagtype='ner') + >>> tokens = 'Rami Eid is studying at Stony Brook University in NY'.split() + >>> parser.tag(tokens) # doctest: +NORMALIZE_WHITESPACE + [('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), ('studying', 'O'), ('at', 'O'), ('Stony', 'ORGANIZATION'), + ('Brook', 'ORGANIZATION'), ('University', 'ORGANIZATION'), ('in', 'O'), ('NY', 'STATE_OR_PROVINCE')] + + >>> parser = CoreNLPParser(url=server.url, tagtype='pos') + >>> tokens = "What is the airspeed of an unladen swallow ?".split() + >>> parser.tag(tokens) # doctest: +NORMALIZE_WHITESPACE + [('What', 'WP'), ('is', 'VBZ'), ('the', 'DT'), + ('airspeed', 'NN'), ('of', 'IN'), ('an', 'DT'), + ('unladen', 'JJ'), ('swallow', 'VB'), ('?', '.')] + >>> server.stop() + """ + return self.tag_sents([sentence], properties)[0] + + def raw_tag_sents(self, sentences, properties=None): + """ + Tag multiple sentences. + + Takes multiple sentences as a list where each sentence is a string. + + :param sentences: Input sentences to tag + :type sentences: list(str) + :rtype: list(list(list(tuple(str, str))) + """ + default_properties = { + "ssplit.isOneSentence": "true", + "annotators": "tokenize,ssplit,", + } + default_properties.update(properties or {}) + + # Supports only 'pos' or 'ner' tags. + assert self.tagtype in [ + "pos", + "ner", + ], "CoreNLP tagger supports only 'pos' or 'ner' tags." + default_properties["annotators"] += self.tagtype + for sentence in sentences: + tagged_data = self.api_call(sentence, properties=default_properties) + yield [ + [ + (token["word"], token[self.tagtype]) + for token in tagged_sentence["tokens"] + ] + for tagged_sentence in tagged_data["sentences"] + ] + + +class CoreNLPParser(GenericCoreNLPParser): + """ + Skip these tests if CoreNLP is likely not ready. + >>> from nltk.test.setup_fixt import check_jar + >>> check_jar(CoreNLPServer._JAR, env_vars=("CORENLP",), is_regex=True) + + The recommended usage of `CoreNLPParser` is using the context manager notation: + >>> with CoreNLPServer() as server: + ... parser = CoreNLPParser(url=server.url) + ... next( + ... parser.raw_parse('The quick brown fox jumps over the lazy dog.') + ... ).pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + _______________|__________________________ + | VP | + | _________|___ | + | | PP | + | | ________|___ | + NP | | NP | + ____|__________ | | _______|____ | + DT JJ JJ NN VBZ IN DT JJ NN . + | | | | | | | | | | + The quick brown fox jumps over the lazy dog . + + Alternatively, the server can be started using the following notation. + Note that `CoreNLPServer` does not need to be used if the CoreNLP server is started + outside of Python. + >>> server = CoreNLPServer() + >>> server.start() + >>> parser = CoreNLPParser(url=server.url) + + >>> (parse_fox, ), (parse_wolf, ) = parser.raw_parse_sents( + ... [ + ... 'The quick brown fox jumps over the lazy dog.', + ... 'The quick grey wolf jumps over the lazy fox.', + ... ] + ... ) + + >>> parse_fox.pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + _______________|__________________________ + | VP | + | _________|___ | + | | PP | + | | ________|___ | + NP | | NP | + ____|__________ | | _______|____ | + DT JJ JJ NN VBZ IN DT JJ NN . + | | | | | | | | | | + The quick brown fox jumps over the lazy dog . + + >>> parse_wolf.pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + _______________|__________________________ + | VP | + | _________|___ | + | | PP | + | | ________|___ | + NP | | NP | + ____|_________ | | _______|____ | + DT JJ JJ NN VBZ IN DT JJ NN . + | | | | | | | | | | + The quick grey wolf jumps over the lazy fox . + + >>> (parse_dog, ), (parse_friends, ) = parser.parse_sents( + ... [ + ... "I 'm a dog".split(), + ... "This is my friends ' cat ( the tabby )".split(), + ... ] + ... ) + + >>> parse_dog.pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + _______|____ + | VP + | ________|___ + NP | NP + | | ___|___ + PRP VBP DT NN + | | | | + I 'm a dog + + >>> parse_friends.pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + ____|___________ + | VP + | ___________|_____________ + | | NP + | | _______|________________________ + | | NP | | | + | | _____|_______ | | | + NP | NP | | NP | + | | ______|_________ | | ___|____ | + DT VBZ PRP$ NNS POS NN -LRB- DT NN -RRB- + | | | | | | | | | | + This is my friends ' cat -LRB- the tabby -RRB- + + >>> parse_john, parse_mary, = parser.parse_text( + ... 'John loves Mary. Mary walks.' + ... ) + + >>> parse_john.pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + _____|_____________ + | VP | + | ____|___ | + NP | NP | + | | | | + NNP VBZ NNP . + | | | | + John loves Mary . + + >>> parse_mary.pretty_print() # doctest: +NORMALIZE_WHITESPACE + ROOT + | + S + _____|____ + NP VP | + | | | + NNP VBZ . + | | | + Mary walks . + + Special cases + + >>> next( + ... parser.raw_parse( + ... 'NASIRIYA, Iraq—Iraqi doctors who treated former prisoner of war ' + ... 'Jessica Lynch have angrily dismissed claims made in her biography ' + ... 'that she was raped by her Iraqi captors.' + ... ) + ... ).height() + 14 + + >>> next( + ... parser.raw_parse( + ... "The broader Standard & Poor's 500 Index <.SPX> was 0.46 points lower, or " + ... '0.05 percent, at 997.02.' + ... ) + ... ).height() + 11 + + >>> server.stop() + """ + + _OUTPUT_FORMAT = "penn" + parser_annotator = "parse" + + def make_tree(self, result): + return Tree.fromstring(result["parse"]) + + +class CoreNLPDependencyParser(GenericCoreNLPParser): + """Dependency parser. + + Skip these tests if CoreNLP is likely not ready. + >>> from nltk.test.setup_fixt import check_jar + >>> check_jar(CoreNLPServer._JAR, env_vars=("CORENLP",), is_regex=True) + + The recommended usage of `CoreNLPParser` is using the context manager notation: + >>> with CoreNLPServer() as server: + ... dep_parser = CoreNLPDependencyParser(url=server.url) + ... parse, = dep_parser.raw_parse( + ... 'The quick brown fox jumps over the lazy dog.' + ... ) + ... print(parse.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + The DT 4 det + quick JJ 4 amod + brown JJ 4 amod + fox NN 5 nsubj + jumps VBZ 0 ROOT + over IN 9 case + the DT 9 det + lazy JJ 9 amod + dog NN 5 obl + . . 5 punct + + Alternatively, the server can be started using the following notation. + Note that `CoreNLPServer` does not need to be used if the CoreNLP server is started + outside of Python. + >>> server = CoreNLPServer() + >>> server.start() + >>> dep_parser = CoreNLPDependencyParser(url=server.url) + >>> parse, = dep_parser.raw_parse('The quick brown fox jumps over the lazy dog.') + >>> print(parse.tree()) # doctest: +NORMALIZE_WHITESPACE + (jumps (fox The quick brown) (dog over the lazy) .) + + >>> for governor, dep, dependent in parse.triples(): + ... print(governor, dep, dependent) # doctest: +NORMALIZE_WHITESPACE + ('jumps', 'VBZ') nsubj ('fox', 'NN') + ('fox', 'NN') det ('The', 'DT') + ('fox', 'NN') amod ('quick', 'JJ') + ('fox', 'NN') amod ('brown', 'JJ') + ('jumps', 'VBZ') obl ('dog', 'NN') + ('dog', 'NN') case ('over', 'IN') + ('dog', 'NN') det ('the', 'DT') + ('dog', 'NN') amod ('lazy', 'JJ') + ('jumps', 'VBZ') punct ('.', '.') + + >>> (parse_fox, ), (parse_dog, ) = dep_parser.raw_parse_sents( + ... [ + ... 'The quick brown fox jumps over the lazy dog.', + ... 'The quick grey wolf jumps over the lazy fox.', + ... ] + ... ) + >>> print(parse_fox.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + The DT 4 det + quick JJ 4 amod + brown JJ 4 amod + fox NN 5 nsubj + jumps VBZ 0 ROOT + over IN 9 case + the DT 9 det + lazy JJ 9 amod + dog NN 5 obl + . . 5 punct + + >>> print(parse_dog.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + The DT 4 det + quick JJ 4 amod + grey JJ 4 amod + wolf NN 5 nsubj + jumps VBZ 0 ROOT + over IN 9 case + the DT 9 det + lazy JJ 9 amod + fox NN 5 obl + . . 5 punct + + >>> (parse_dog, ), (parse_friends, ) = dep_parser.parse_sents( + ... [ + ... "I 'm a dog".split(), + ... "This is my friends ' cat ( the tabby )".split(), + ... ] + ... ) + >>> print(parse_dog.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + I PRP 4 nsubj + 'm VBP 4 cop + a DT 4 det + dog NN 0 ROOT + + >>> print(parse_friends.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + This DT 6 nsubj + is VBZ 6 cop + my PRP$ 4 nmod:poss + friends NNS 6 nmod:poss + ' POS 4 case + cat NN 0 ROOT + ( -LRB- 9 punct + the DT 9 det + tabby NN 6 dep + ) -RRB- 9 punct + + >>> parse_john, parse_mary, = dep_parser.parse_text( + ... 'John loves Mary. Mary walks.' + ... ) + + >>> print(parse_john.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + John NNP 2 nsubj + loves VBZ 0 ROOT + Mary NNP 2 obj + . . 2 punct + + >>> print(parse_mary.to_conll(4)) # doctest: +NORMALIZE_WHITESPACE + Mary NNP 2 nsubj + walks VBZ 0 ROOT + . . 2 punct + + Special cases + + Non-breaking space inside of a token. + + >>> len( + ... next( + ... dep_parser.raw_parse( + ... 'Anhalt said children typically treat a 20-ounce soda bottle as one ' + ... 'serving, while it actually contains 2 1/2 servings.' + ... ) + ... ).nodes + ... ) + 23 + + Phone numbers. + + >>> len( + ... next( + ... dep_parser.raw_parse('This is not going to crash: 01 111 555.') + ... ).nodes + ... ) + 10 + + >>> print( + ... next( + ... dep_parser.raw_parse('The underscore _ should not simply disappear.') + ... ).to_conll(4) + ... ) # doctest: +NORMALIZE_WHITESPACE + The DT 2 det + underscore NN 7 nsubj + _ NFP 7 punct + should MD 7 aux + not RB 7 advmod + simply RB 7 advmod + disappear VB 0 ROOT + . . 7 punct + + >>> print( + ... next( + ... dep_parser.raw_parse( + ... 'for all of its insights into the dream world of teen life , and its electronic expression through ' + ... 'cyber culture , the film gives no quarter to anyone seeking to pull a cohesive story out of its 2 ' + ... '1/2-hour running time .' + ... ) + ... ).to_conll(4) + ... ) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS + for IN 2 case + all DT 24 obl + of IN 5 case + its PRP$ 5 nmod:poss + insights NNS 2 nmod + into IN 9 case + the DT 9 det + dream NN 9 compound + world NN 5 nmod + of IN 12 case + teen NN 12 compound + ... + + >>> server.stop() + """ + + _OUTPUT_FORMAT = "conll2007" + parser_annotator = "depparse" + + def make_tree(self, result): + return DependencyGraph( + ( + " ".join(n_items[1:]) # NLTK expects an iterable of strings... + for n_items in sorted(transform(result)) + ), + cell_separator=" ", # To make sure that a non-breaking space is kept inside of a token. + ) + + +def transform(sentence): + for dependency in sentence["basicDependencies"]: + dependent_index = dependency["dependent"] + token = sentence["tokens"][dependent_index - 1] + + # Return values that we don't know as '_'. Also, consider tag and ctag + # to be equal. + yield ( + dependent_index, + "_", + token["word"], + token["lemma"], + token["pos"], + token["pos"], + "_", + str(dependency["governor"]), + dependency["dep"], + "_", + "_", + ) diff --git a/nltk/parse/dependencygraph.py b/nltk/parse/dependencygraph.py new file mode 100755 index 0000000..0e0708a --- /dev/null +++ b/nltk/parse/dependencygraph.py @@ -0,0 +1,821 @@ +# Natural Language Toolkit: Dependency Grammars +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Jason Narad +# Steven Bird (modifications) +# +# URL: +# For license information, see LICENSE.TXT +# + +""" +Tools for reading and writing dependency trees. +The input is assumed to be in Malt-TAB format +(https://stp.lingfil.uu.se/~nivre/research/MaltXML.html). +""" + +import subprocess +import warnings +from collections import defaultdict +from itertools import chain +from pprint import pformat + +from nltk.internals import find_binary +from nltk.pathsec import open as _secure_open +from nltk.tree import Tree + +################################################################# +# DependencyGraph Class +################################################################# + + +class DependencyGraph: + """ + A container for the nodes and labelled edges of a dependency structure. + """ + + def __init__( + self, + tree_str=None, + cell_extractor=None, + zero_based=False, + cell_separator=None, + top_relation_label="ROOT", + ): + """Dependency graph. + + We place a dummy `TOP` node with the index 0, since the root node is + often assigned 0 as its head. This also means that the indexing of the + nodes corresponds directly to the Malt-TAB format, which starts at 1. + + If zero-based is True, then Malt-TAB-like input with node numbers + starting at 0 and the root node assigned -1 (as produced by, e.g., + zpar). + + :param str cell_separator: the cell separator. If not provided, cells + are split by whitespace. + + :param str top_relation_label: the label by which the top relation is + identified, for examlple, `ROOT`, `null` or `TOP`. + """ + self.nodes = defaultdict( + lambda: { + "address": None, + "word": None, + "lemma": None, + "ctag": None, + "tag": None, + "feats": None, + "head": None, + "deps": defaultdict(list), + "rel": None, + } + ) + + self.nodes[0].update({"ctag": "TOP", "tag": "TOP", "address": 0}) + + self.root = None + + if tree_str: + self._parse( + tree_str, + cell_extractor=cell_extractor, + zero_based=zero_based, + cell_separator=cell_separator, + top_relation_label=top_relation_label, + ) + + def remove_by_address(self, address): + """ + Removes the node with the given address. References + to this node in others will still exist. + """ + del self.nodes[address] + + def redirect_arcs(self, originals, redirect): + """ + Redirects arcs to any of the nodes in the originals list + to the redirect node address. + """ + for node in self.nodes.values(): + new_deps = [] + for dep in node["deps"]: + if dep in originals: + new_deps.append(redirect) + else: + new_deps.append(dep) + node["deps"] = new_deps + + def add_arc(self, head_address, mod_address): + """ + Adds an arc from the node specified by head_address to the + node specified by the mod address. + """ + relation = self.nodes[mod_address]["rel"] + self.nodes[head_address]["deps"].setdefault(relation, []) + self.nodes[head_address]["deps"][relation].append(mod_address) + # self.nodes[head_address]['deps'].append(mod_address) + + def connect_graph(self): + """ + Fully connects all non-root nodes. All nodes are set to be dependents + of the root node. + """ + for node1 in self.nodes.values(): + for node2 in self.nodes.values(): + if node1["address"] != node2["address"] and node2["rel"] != "TOP": + relation = node2["rel"] + node1["deps"].setdefault(relation, []) + node1["deps"][relation].append(node2["address"]) + # node1['deps'].append(node2['address']) + + def get_by_address(self, node_address): + """Return the node with the given address.""" + return self.nodes[node_address] + + def contains_address(self, node_address): + """ + Returns true if the graph contains a node with the given node + address, false otherwise. + """ + return node_address in self.nodes + + def to_dot(self): + """Return a dot representation suitable for using with Graphviz. + + >>> dg = DependencyGraph( + ... 'John N 2\\n' + ... 'loves V 0\\n' + ... 'Mary N 2' + ... ) + >>> print(dg.to_dot()) + digraph G{ + edge [dir=forward] + node [shape=plaintext] + + 0 [label="0 (None)"] + 0 -> 2 [label="ROOT"] + 1 [label="1 (John)"] + 2 [label="2 (loves)"] + 2 -> 1 [label=""] + 2 -> 3 [label=""] + 3 [label="3 (Mary)"] + } + + """ + # Start the digraph specification + s = "digraph G{\n" + s += "edge [dir=forward]\n" + s += "node [shape=plaintext]\n" + + # Draw the remaining nodes + for node in sorted(self.nodes.values(), key=lambda v: v["address"]): + s += '\n{} [label="{} ({})"]'.format( + node["address"], + node["address"], + node["word"], + ) + for rel, deps in node["deps"].items(): + for dep in deps: + if rel is not None: + s += '\n{} -> {} [label="{}"]'.format(node["address"], dep, rel) + else: + s += "\n{} -> {} ".format(node["address"], dep) + s += "\n}" + + return s + + def _repr_svg_(self): + """Show SVG representation of the transducer (IPython magic). + >>> from nltk.test.setup_fixt import check_binary + >>> check_binary('dot') + >>> dg = DependencyGraph( + ... 'John N 2\\n' + ... 'loves V 0\\n' + ... 'Mary N 2' + ... ) + >>> dg._repr_svg_().split('\\n')[0] + '' + + """ + dot_string = self.to_dot() + return dot2img(dot_string) + + def __str__(self): + return pformat(self.nodes) + + def __repr__(self): + return f"" + + @staticmethod + def load( + filename, zero_based=False, cell_separator=None, top_relation_label="ROOT" + ): + """ + :param filename: a name of a file in Malt-TAB format + :param zero_based: nodes in the input file are numbered starting from 0 + rather than 1 (as produced by, e.g., zpar) + :param str cell_separator: the cell separator. If not provided, cells + are split by whitespace. + :param str top_relation_label: the label by which the top relation is + identified, for examlple, `ROOT`, `null` or `TOP`. + + :return: a list of DependencyGraphs + + """ + # Route through the pathsec sentinel so the read honours the file-access + # sandbox (allowed data roots, symlink resolution) instead of the builtin + # open, which bypasses it and can read arbitrary local files (CWE-22). + with _secure_open(filename) as infile: + return [ + DependencyGraph( + tree_str, + zero_based=zero_based, + cell_separator=cell_separator, + top_relation_label=top_relation_label, + ) + for tree_str in infile.read().split("\n\n") + ] + + def left_children(self, node_index): + """ + Returns the number of left children under the node specified + by the given address. + """ + children = chain.from_iterable(self.nodes[node_index]["deps"].values()) + index = self.nodes[node_index]["address"] + return sum(1 for c in children if c < index) + + def right_children(self, node_index): + """ + Returns the number of right children under the node specified + by the given address. + """ + children = chain.from_iterable(self.nodes[node_index]["deps"].values()) + index = self.nodes[node_index]["address"] + return sum(1 for c in children if c > index) + + def add_node(self, node): + if not self.contains_address(node["address"]): + self.nodes[node["address"]].update(node) + + def _parse( + self, + input_, + cell_extractor=None, + zero_based=False, + cell_separator=None, + top_relation_label="ROOT", + ): + """Parse a sentence. + + :param extractor: a function that given a tuple of cells returns a + 7-tuple, where the values are ``word, lemma, ctag, tag, feats, head, + rel``. + + :param str cell_separator: the cell separator. If not provided, cells + are split by whitespace. + + :param str top_relation_label: the label by which the top relation is + identified, for examlple, `ROOT`, `null` or `TOP`. + + """ + + def extract_3_cells(cells, index): + word, tag, head = cells + return index, word, word, tag, tag, "", head, "" + + def extract_4_cells(cells, index): + word, tag, head, rel = cells + return index, word, word, tag, tag, "", head, rel + + def extract_7_cells(cells, index): + line_index, word, lemma, tag, _, head, rel = cells + try: + index = int(line_index) + except ValueError: + # index can't be parsed as an integer, use default + pass + return index, word, lemma, tag, tag, "", head, rel + + def extract_10_cells(cells, index): + line_index, word, lemma, ctag, tag, feats, head, rel, _, _ = cells + try: + index = int(line_index) + except ValueError: + # index can't be parsed as an integer, use default + pass + return index, word, lemma, ctag, tag, feats, head, rel + + extractors = { + 3: extract_3_cells, + 4: extract_4_cells, + 7: extract_7_cells, + 10: extract_10_cells, + } + + if isinstance(input_, str): + input_ = (line for line in input_.split("\n")) + + lines = (l.rstrip() for l in input_) + lines = (l for l in lines if l) + + cell_number = None + for index, line in enumerate(lines, start=1): + cells = line.split(cell_separator) + if cell_number is None: + cell_number = len(cells) + else: + assert cell_number == len(cells) + + if cell_extractor is None: + try: + cell_extractor = extractors[cell_number] + except KeyError as e: + raise ValueError( + "Number of tab-delimited fields ({}) not supported by " + "CoNLL(10) or Malt-Tab(4) format".format(cell_number) + ) from e + + try: + index, word, lemma, ctag, tag, feats, head, rel = cell_extractor( + cells, index + ) + except (TypeError, ValueError): + # cell_extractor doesn't take 2 arguments or doesn't return 8 + # values; assume the cell_extractor is an older external + # extractor and doesn't accept or return an index. + word, lemma, ctag, tag, feats, head, rel = cell_extractor(cells) + + if head == "_": + continue + + head = int(head) + if zero_based: + head += 1 + + self.nodes[index].update( + { + "address": index, + "word": word, + "lemma": lemma, + "ctag": ctag, + "tag": tag, + "feats": feats, + "head": head, + "rel": rel, + } + ) + + # Make sure that the fake root node has labeled dependencies. + if (cell_number == 3) and (head == 0): + rel = top_relation_label + self.nodes[head]["deps"][rel].append(index) + + if self.nodes[0]["deps"][top_relation_label]: + root_address = self.nodes[0]["deps"][top_relation_label][0] + self.root = self.nodes[root_address] + self.top_relation_label = top_relation_label + else: + warnings.warn( + "The graph doesn't contain a node " "that depends on the root element." + ) + + def _word(self, node, filter=True): + w = node["word"] + if filter: + if w != ",": + return w + return w + + def _tree(self, i): + """Turn dependency graphs into NLTK trees. + + :param int i: index of a node + :return: either a word (if the indexed node is a leaf) or a ``Tree``. + """ + node = self.get_by_address(i) + word = node["word"] + deps = sorted(chain.from_iterable(node["deps"].values())) + + if deps: + return Tree(word, [self._tree(dep) for dep in deps]) + else: + return word + + def tree(self): + """ + Starting with the ``root`` node, build a dependency tree using the NLTK + ``Tree`` constructor. Dependency labels are omitted. + """ + node = self.root + + word = node["word"] + deps = sorted(chain.from_iterable(node["deps"].values())) + return Tree(word, [self._tree(dep) for dep in deps]) + + def triples(self, node=None): + """ + Extract dependency triples of the form: + ((head word, head tag), rel, (dep word, dep tag)) + """ + + if not node: + node = self.root + + head = (node["word"], node["ctag"]) + for i in sorted(chain.from_iterable(node["deps"].values())): + dep = self.get_by_address(i) + yield (head, dep["rel"], (dep["word"], dep["ctag"])) + yield from self.triples(node=dep) + + def _hd(self, i): + try: + return self.nodes[i]["head"] + except IndexError: + return None + + def _rel(self, i): + try: + return self.nodes[i]["rel"] + except IndexError: + return None + + # what's the return type? Boolean or list? + def contains_cycle(self): + """Check whether there are cycles. + + >>> dg = DependencyGraph(treebank_data) + >>> dg.contains_cycle() + False + + >>> cyclic_dg = DependencyGraph() + >>> top = {'word': None, 'deps': [1], 'rel': 'TOP', 'address': 0} + >>> child1 = {'word': None, 'deps': [2], 'rel': 'NTOP', 'address': 1} + >>> child2 = {'word': None, 'deps': [4], 'rel': 'NTOP', 'address': 2} + >>> child3 = {'word': None, 'deps': [1], 'rel': 'NTOP', 'address': 3} + >>> child4 = {'word': None, 'deps': [3], 'rel': 'NTOP', 'address': 4} + >>> cyclic_dg.nodes = { + ... 0: top, + ... 1: child1, + ... 2: child2, + ... 3: child3, + ... 4: child4, + ... } + >>> cyclic_dg.root = top + + >>> cyclic_dg.contains_cycle() + [1, 2, 4, 3] + + """ + # Detect a directed cycle with an iterative depth-first search in + # O(V + E). The previous implementation repeatedly recomputed a + # transitive closure with a triple-nested loop over a ``distances`` + # set that grows quadratically in the number of nodes, so its running + # time grew as a high-degree polynomial in the (attacker-controlled) + # number of tokens -- roughly cubic on a graph parsed from CoNLL data + # and quintic on a list-``deps`` graph -- versus O(V + E) here, which + # let a single oversized graph exhaust CPU (CWE-407). + # + # ``deps`` may be a list of addresses or a relation->addresses + # mapping; iterating it yields the same edge targets the old code + # used. The membership guard skips targets that are not nodes and + # avoids materialising spurious ``defaultdict`` entries in + # ``self.nodes``. + WHITE, GRAY, BLACK = 0, 1, 2 + color = defaultdict(int) # int() == WHITE + + for start in list(self.nodes): + if color[start] != WHITE: + continue + color[start] = GRAY + path = [start] + stack = [iter(self.nodes[start]["deps"])] + while stack: + for dep in stack[-1]: + if dep not in self.nodes: + continue + if color[dep] == GRAY: + # Back edge: the gray nodes from ``dep`` onward form + # the cycle, in traversal order. + return path[path.index(dep) :] + if color[dep] == WHITE: + color[dep] = GRAY + path.append(dep) + stack.append(iter(self.nodes[dep]["deps"])) + break + else: + color[path.pop()] = BLACK + stack.pop() + + return False + + def get_cycle_path(self, curr_node, goal_node_index): + for dep in curr_node["deps"]: + if dep == goal_node_index: + return [curr_node["address"]] + for dep in curr_node["deps"]: + path = self.get_cycle_path(self.get_by_address(dep), goal_node_index) + if len(path) > 0: + path.insert(0, curr_node["address"]) + return path + return [] + + def to_conll(self, style): + """ + The dependency graph in CoNLL format. + + :param style: the style to use for the format (3, 4, 10 columns) + :type style: int + :rtype: str + """ + + if style == 3: + template = "{word}\t{tag}\t{head}\n" + elif style == 4: + template = "{word}\t{tag}\t{head}\t{rel}\n" + elif style == 10: + template = ( + "{i}\t{word}\t{lemma}\t{ctag}\t{tag}\t{feats}\t{head}\t{rel}\t_\t_\n" + ) + else: + raise ValueError( + "Number of tab-delimited fields ({}) not supported by " + "CoNLL(10) or Malt-Tab(4) format".format(style) + ) + + return "".join( + template.format(i=i, **node) + for i, node in sorted(self.nodes.items()) + if node["tag"] != "TOP" + ) + + def nx_graph(self): + """Convert the data in a ``nodelist`` into a networkx labeled directed graph.""" + import networkx + + nx_nodelist = list(range(1, len(self.nodes))) + nx_edgelist = [ + (n, self._hd(n), self._rel(n)) for n in nx_nodelist if self._hd(n) + ] + self.nx_labels = {} + for n in nx_nodelist: + self.nx_labels[n] = self.nodes[n]["word"] + + g = networkx.MultiDiGraph() + g.add_nodes_from(nx_nodelist) + g.add_edges_from(nx_edgelist) + + return g + + +def dot2img(dot_string, t="svg"): + """ + Create image representation fom dot_string, using the 'dot' program + from the Graphviz package. + + Use the 't' argument to specify the image file format, for ex. 'jpeg', 'eps', + 'json', 'png' or 'webp' (Running 'dot -T:' lists all available formats). + + Note that the "capture_output" option of subprocess.run() is only available + with text formats (like svg), but not with binary image formats (like png). + """ + + try: + find_binary("dot") + try: + if t in ["dot", "dot_json", "json", "svg"]: + proc = subprocess.run( + ["dot", "-T%s" % t], + capture_output=True, + input=dot_string, + text=True, + ) + else: + proc = subprocess.run( + ["dot", "-T%s" % t], + input=bytes(dot_string, encoding="utf8"), + ) + return proc.stdout + except Exception: + raise Exception( + "Cannot create image representation by running dot from string: {}" + "".format(dot_string) + ) + except OSError as e: + raise Exception("Cannot find the dot binary from Graphviz package") from e + + +class DependencyGraphError(Exception): + """Dependency graph exception.""" + + +def demo(): + malt_demo() + conll_demo() + conll_file_demo() + cycle_finding_demo() + + +def malt_demo(nx=False): + """ + A demonstration of the result of reading a dependency + version of the first sentence of the Penn Treebank. + """ + dg = DependencyGraph( + """Pierre NNP 2 NMOD +Vinken NNP 8 SUB +, , 2 P +61 CD 5 NMOD +years NNS 6 AMOD +old JJ 2 NMOD +, , 2 P +will MD 0 ROOT +join VB 8 VC +the DT 11 NMOD +board NN 9 OBJ +as IN 9 VMOD +a DT 15 NMOD +nonexecutive JJ 15 NMOD +director NN 12 PMOD +Nov. NNP 9 VMOD +29 CD 16 NMOD +. . 9 VMOD +""" + ) + tree = dg.tree() + tree.pprint() + if nx: + # currently doesn't work + import networkx + from matplotlib import pylab + + g = dg.nx_graph() + g.info() + pos = networkx.spring_layout(g, dim=1) + networkx.draw_networkx_nodes(g, pos, node_size=50) + # networkx.draw_networkx_edges(g, pos, edge_color='k', width=8) + networkx.draw_networkx_labels(g, pos, dg.nx_labels) + pylab.xticks([]) + pylab.yticks([]) + pylab.savefig("tree.png") + pylab.show() + + +def conll_demo(): + """ + A demonstration of how to read a string representation of + a CoNLL format dependency tree. + """ + dg = DependencyGraph(conll_data1) + tree = dg.tree() + tree.pprint() + print(dg) + print(dg.to_conll(4)) + + +def conll_file_demo(): + print("Mass conll_read demo...") + graphs = [DependencyGraph(entry) for entry in conll_data2.split("\n\n") if entry] + for graph in graphs: + tree = graph.tree() + print("\n") + tree.pprint() + + +def cycle_finding_demo(): + dg = DependencyGraph(treebank_data) + print(dg.contains_cycle()) + cyclic_dg = DependencyGraph() + cyclic_dg.add_node({"word": None, "deps": [1], "rel": "TOP", "address": 0}) + cyclic_dg.add_node({"word": None, "deps": [2], "rel": "NTOP", "address": 1}) + cyclic_dg.add_node({"word": None, "deps": [4], "rel": "NTOP", "address": 2}) + cyclic_dg.add_node({"word": None, "deps": [1], "rel": "NTOP", "address": 3}) + cyclic_dg.add_node({"word": None, "deps": [3], "rel": "NTOP", "address": 4}) + print(cyclic_dg.contains_cycle()) + + +treebank_data = """Pierre NNP 2 NMOD +Vinken NNP 8 SUB +, , 2 P +61 CD 5 NMOD +years NNS 6 AMOD +old JJ 2 NMOD +, , 2 P +will MD 0 ROOT +join VB 8 VC +the DT 11 NMOD +board NN 9 OBJ +as IN 9 VMOD +a DT 15 NMOD +nonexecutive JJ 15 NMOD +director NN 12 PMOD +Nov. NNP 9 VMOD +29 CD 16 NMOD +. . 9 VMOD +""" + +conll_data1 = """ +1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ +2 had heb V V trans|ovt|1of2of3|ev 0 ROOT _ _ +3 met met Prep Prep voor 8 mod _ _ +4 haar haar Pron Pron bez|3|ev|neut|attr 5 det _ _ +5 moeder moeder N N soort|ev|neut 3 obj1 _ _ +6 kunnen kan V V hulp|ott|1of2of3|mv 2 vc _ _ +7 gaan ga V V hulp|inf 6 vc _ _ +8 winkelen winkel V V intrans|inf 11 cnj _ _ +9 , , Punc Punc komma 8 punct _ _ +10 zwemmen zwem V V intrans|inf 11 cnj _ _ +11 of of Conj Conj neven 7 vc _ _ +12 terrassen terras N N soort|mv|neut 11 cnj _ _ +13 . . Punc Punc punt 12 punct _ _ +""" + +conll_data2 = """1 Cathy Cathy N N eigen|ev|neut 2 su _ _ +2 zag zie V V trans|ovt|1of2of3|ev 0 ROOT _ _ +3 hen hen Pron Pron per|3|mv|datofacc 2 obj1 _ _ +4 wild wild Adj Adj attr|stell|onverv 5 mod _ _ +5 zwaaien zwaai N N soort|mv|neut 2 vc _ _ +6 . . Punc Punc punt 5 punct _ _ + +1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ +2 had heb V V trans|ovt|1of2of3|ev 0 ROOT _ _ +3 met met Prep Prep voor 8 mod _ _ +4 haar haar Pron Pron bez|3|ev|neut|attr 5 det _ _ +5 moeder moeder N N soort|ev|neut 3 obj1 _ _ +6 kunnen kan V V hulp|ott|1of2of3|mv 2 vc _ _ +7 gaan ga V V hulp|inf 6 vc _ _ +8 winkelen winkel V V intrans|inf 11 cnj _ _ +9 , , Punc Punc komma 8 punct _ _ +10 zwemmen zwem V V intrans|inf 11 cnj _ _ +11 of of Conj Conj neven 7 vc _ _ +12 terrassen terras N N soort|mv|neut 11 cnj _ _ +13 . . Punc Punc punt 12 punct _ _ + +1 Dat dat Pron Pron aanw|neut|attr 2 det _ _ +2 werkwoord werkwoord N N soort|ev|neut 6 obj1 _ _ +3 had heb V V hulp|ovt|1of2of3|ev 0 ROOT _ _ +4 ze ze Pron Pron per|3|evofmv|nom 6 su _ _ +5 zelf zelf Pron Pron aanw|neut|attr|wzelf 3 predm _ _ +6 uitgevonden vind V V trans|verldw|onverv 3 vc _ _ +7 . . Punc Punc punt 6 punct _ _ + +1 Het het Pron Pron onbep|neut|zelfst 2 su _ _ +2 hoorde hoor V V trans|ovt|1of2of3|ev 0 ROOT _ _ +3 bij bij Prep Prep voor 2 ld _ _ +4 de de Art Art bep|zijdofmv|neut 6 det _ _ +5 warme warm Adj Adj attr|stell|vervneut 6 mod _ _ +6 zomerdag zomerdag N N soort|ev|neut 3 obj1 _ _ +7 die die Pron Pron betr|neut|zelfst 6 mod _ _ +8 ze ze Pron Pron per|3|evofmv|nom 12 su _ _ +9 ginds ginds Adv Adv gew|aanw 12 mod _ _ +10 achter achter Adv Adv gew|geenfunc|stell|onverv 12 svp _ _ +11 had heb V V hulp|ovt|1of2of3|ev 7 body _ _ +12 gelaten laat V V trans|verldw|onverv 11 vc _ _ +13 . . Punc Punc punt 12 punct _ _ + +1 Ze ze Pron Pron per|3|evofmv|nom 2 su _ _ +2 hadden heb V V trans|ovt|1of2of3|mv 0 ROOT _ _ +3 languit languit Adv Adv gew|geenfunc|stell|onverv 11 mod _ _ +4 naast naast Prep Prep voor 11 mod _ _ +5 elkaar elkaar Pron Pron rec|neut 4 obj1 _ _ +6 op op Prep Prep voor 11 ld _ _ +7 de de Art Art bep|zijdofmv|neut 8 det _ _ +8 strandstoelen strandstoel N N soort|mv|neut 6 obj1 _ _ +9 kunnen kan V V hulp|inf 2 vc _ _ +10 gaan ga V V hulp|inf 9 vc _ _ +11 liggen lig V V intrans|inf 10 vc _ _ +12 . . Punc Punc punt 11 punct _ _ + +1 Zij zij Pron Pron per|3|evofmv|nom 2 su _ _ +2 zou zal V V hulp|ovt|1of2of3|ev 7 cnj _ _ +3 mams mams N N soort|ev|neut 4 det _ _ +4 rug rug N N soort|ev|neut 5 obj1 _ _ +5 ingewreven wrijf V V trans|verldw|onverv 6 vc _ _ +6 hebben heb V V hulp|inf 2 vc _ _ +7 en en Conj Conj neven 0 ROOT _ _ +8 mam mam V V trans|ovt|1of2of3|ev 7 cnj _ _ +9 de de Art Art bep|zijdofmv|neut 10 det _ _ +10 hare hare Pron Pron bez|3|ev|neut|attr 8 obj1 _ _ +11 . . Punc Punc punt 10 punct _ _ + +1 Of of Conj Conj onder|metfin 0 ROOT _ _ +2 ze ze Pron Pron per|3|evofmv|nom 3 su _ _ +3 had heb V V hulp|ovt|1of2of3|ev 0 ROOT _ _ +4 gewoon gewoon Adj Adj adv|stell|onverv 10 mod _ _ +5 met met Prep Prep voor 10 mod _ _ +6 haar haar Pron Pron bez|3|ev|neut|attr 7 det _ _ +7 vriendinnen vriendin N N soort|mv|neut 5 obj1 _ _ +8 rond rond Adv Adv deelv 10 svp _ _ +9 kunnen kan V V hulp|inf 3 vc _ _ +10 slenteren slenter V V intrans|inf 9 vc _ _ +11 in in Prep Prep voor 10 mod _ _ +12 de de Art Art bep|zijdofmv|neut 13 det _ _ +13 buurt buurt N N soort|ev|neut 11 obj1 _ _ +14 van van Prep Prep voor 13 mod _ _ +15 Trafalgar_Square Trafalgar_Square MWU N_N eigen|ev|neut_eigen|ev|neut 14 obj1 _ _ +16 . . Punc Punc punt 15 punct _ _ +""" + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/earleychart.py b/nltk/parse/earleychart.py new file mode 100644 index 0000000..c658f51 --- /dev/null +++ b/nltk/parse/earleychart.py @@ -0,0 +1,552 @@ +# Natural Language Toolkit: An Incremental Earley Chart Parser +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Peter Ljunglöf +# Rob Speer +# Edward Loper +# Steven Bird +# Jean Mark Gawron +# URL: +# For license information, see LICENSE.TXT + +""" +Data classes and parser implementations for *incremental* chart +parsers, which use dynamic programming to efficiently parse a text. +A "chart parser" derives parse trees for a text by iteratively adding +\"edges\" to a \"chart\". Each "edge" represents a hypothesis about the tree +structure for a subsequence of the text. The "chart" is a +\"blackboard\" for composing and combining these hypotheses. + +A parser is "incremental", if it guarantees that for all i, j where i < j, +all edges ending at i are built before any edges ending at j. +This is appealing for, say, speech recognizer hypothesis filtering. + +The main parser class is ``EarleyChartParser``, which is a top-down +algorithm, originally formulated by Jay Earley (1970). +""" + +from time import perf_counter + +from nltk.parse.chart import ( + BottomUpPredictCombineRule, + BottomUpPredictRule, + CachedTopDownPredictRule, + Chart, + ChartParser, + EdgeI, + EmptyPredictRule, + FilteredBottomUpPredictCombineRule, + FilteredSingleEdgeFundamentalRule, + LeafEdge, + LeafInitRule, + SingleEdgeFundamentalRule, + TopDownInitRule, +) +from nltk.parse.featurechart import ( + FeatureBottomUpPredictCombineRule, + FeatureBottomUpPredictRule, + FeatureChart, + FeatureChartParser, + FeatureEmptyPredictRule, + FeatureSingleEdgeFundamentalRule, + FeatureTopDownInitRule, + FeatureTopDownPredictRule, +) + +# //////////////////////////////////////////////////////////// +# Incremental Chart +# //////////////////////////////////////////////////////////// + + +class IncrementalChart(Chart): + def initialize(self): + # A sequence of edge lists contained in this chart. + self._edgelists = tuple([] for x in self._positions()) + + # The set of child pointer lists associated with each edge. + self._edge_to_cpls = {} + + # Indexes mapping attribute values to lists of edges + # (used by select()). + self._indexes = {} + + def edges(self): + return list(self.iteredges()) + + def iteredges(self): + return (edge for edgelist in self._edgelists for edge in edgelist) + + def select(self, end, **restrictions): + edgelist = self._edgelists[end] + + # If there are no restrictions, then return all edges. + if restrictions == {}: + return iter(edgelist) + + # Find the index corresponding to the given restrictions. + restr_keys = sorted(restrictions.keys()) + restr_keys = tuple(restr_keys) + + # If it doesn't exist, then create it. + if restr_keys not in self._indexes: + self._add_index(restr_keys) + + vals = tuple(restrictions[key] for key in restr_keys) + return iter(self._indexes[restr_keys][end].get(vals, [])) + + def _add_index(self, restr_keys): + # Make sure it's a valid index. + for key in restr_keys: + if not hasattr(EdgeI, key): + raise ValueError("Bad restriction: %s" % key) + + # Create the index. + index = self._indexes[restr_keys] = tuple({} for x in self._positions()) + + # Add all existing edges to the index. + for end, edgelist in enumerate(self._edgelists): + this_index = index[end] + for edge in edgelist: + vals = tuple(getattr(edge, key)() for key in restr_keys) + this_index.setdefault(vals, []).append(edge) + + def _register_with_indexes(self, edge): + end = edge.end() + for restr_keys, index in self._indexes.items(): + vals = tuple(getattr(edge, key)() for key in restr_keys) + index[end].setdefault(vals, []).append(edge) + + def _append_edge(self, edge): + self._edgelists[edge.end()].append(edge) + + def _positions(self): + return range(self.num_leaves() + 1) + + +class FeatureIncrementalChart(IncrementalChart, FeatureChart): + def select(self, end, **restrictions): + edgelist = self._edgelists[end] + + # If there are no restrictions, then return all edges. + if restrictions == {}: + return iter(edgelist) + + # Find the index corresponding to the given restrictions. + restr_keys = sorted(restrictions.keys()) + restr_keys = tuple(restr_keys) + + # If it doesn't exist, then create it. + if restr_keys not in self._indexes: + self._add_index(restr_keys) + + vals = tuple( + self._get_type_if_possible(restrictions[key]) for key in restr_keys + ) + return iter(self._indexes[restr_keys][end].get(vals, [])) + + def _add_index(self, restr_keys): + # Make sure it's a valid index. + for key in restr_keys: + if not hasattr(EdgeI, key): + raise ValueError("Bad restriction: %s" % key) + + # Create the index. + index = self._indexes[restr_keys] = tuple({} for x in self._positions()) + + # Add all existing edges to the index. + for end, edgelist in enumerate(self._edgelists): + this_index = index[end] + for edge in edgelist: + vals = tuple( + self._get_type_if_possible(getattr(edge, key)()) + for key in restr_keys + ) + this_index.setdefault(vals, []).append(edge) + + def _register_with_indexes(self, edge): + end = edge.end() + for restr_keys, index in self._indexes.items(): + vals = tuple( + self._get_type_if_possible(getattr(edge, key)()) for key in restr_keys + ) + index[end].setdefault(vals, []).append(edge) + + +# //////////////////////////////////////////////////////////// +# Incremental CFG Rules +# //////////////////////////////////////////////////////////// + + +class CompleteFundamentalRule(SingleEdgeFundamentalRule): + def _apply_incomplete(self, chart, grammar, left_edge): + end = left_edge.end() + # When the chart is incremental, we only have to look for + # empty complete edges here. + for right_edge in chart.select( + start=end, end=end, is_complete=True, lhs=left_edge.nextsym() + ): + new_edge = left_edge.move_dot_forward(right_edge.end()) + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + +class CompleterRule(CompleteFundamentalRule): + _fundamental_rule = CompleteFundamentalRule() + + def apply(self, chart, grammar, edge): + if not isinstance(edge, LeafEdge): + yield from self._fundamental_rule.apply(chart, grammar, edge) + + +class ScannerRule(CompleteFundamentalRule): + _fundamental_rule = CompleteFundamentalRule() + + def apply(self, chart, grammar, edge): + if isinstance(edge, LeafEdge): + yield from self._fundamental_rule.apply(chart, grammar, edge) + + +class PredictorRule(CachedTopDownPredictRule): + pass + + +class FilteredCompleteFundamentalRule(FilteredSingleEdgeFundamentalRule): + def apply(self, chart, grammar, edge): + # Since the Filtered rule only works for grammars without empty productions, + # we only have to bother with complete edges here. + if edge.is_complete(): + yield from self._apply_complete(chart, grammar, edge) + + +# //////////////////////////////////////////////////////////// +# Incremental FCFG Rules +# //////////////////////////////////////////////////////////// + + +class FeatureCompleteFundamentalRule(FeatureSingleEdgeFundamentalRule): + def _apply_incomplete(self, chart, grammar, left_edge): + fr = self._fundamental_rule + end = left_edge.end() + # When the chart is incremental, we only have to look for + # empty complete edges here. + for right_edge in chart.select( + start=end, end=end, is_complete=True, lhs=left_edge.nextsym() + ): + yield from fr.apply(chart, grammar, left_edge, right_edge) + + +class FeatureCompleterRule(CompleterRule): + _fundamental_rule = FeatureCompleteFundamentalRule() + + +class FeatureScannerRule(ScannerRule): + _fundamental_rule = FeatureCompleteFundamentalRule() + + +class FeaturePredictorRule(FeatureTopDownPredictRule): + pass + + +# //////////////////////////////////////////////////////////// +# Incremental CFG Chart Parsers +# //////////////////////////////////////////////////////////// + +EARLEY_STRATEGY = [ + LeafInitRule(), + TopDownInitRule(), + CompleterRule(), + ScannerRule(), + PredictorRule(), +] +TD_INCREMENTAL_STRATEGY = [ + LeafInitRule(), + TopDownInitRule(), + CachedTopDownPredictRule(), + CompleteFundamentalRule(), +] +BU_INCREMENTAL_STRATEGY = [ + LeafInitRule(), + EmptyPredictRule(), + BottomUpPredictRule(), + CompleteFundamentalRule(), +] +BU_LC_INCREMENTAL_STRATEGY = [ + LeafInitRule(), + EmptyPredictRule(), + BottomUpPredictCombineRule(), + CompleteFundamentalRule(), +] + +LC_INCREMENTAL_STRATEGY = [ + LeafInitRule(), + FilteredBottomUpPredictCombineRule(), + FilteredCompleteFundamentalRule(), +] + + +class IncrementalChartParser(ChartParser): + """ + An *incremental* chart parser implementing Jay Earley's + parsing algorithm: + + | For each index end in [0, 1, ..., N]: + | For each edge such that edge.end = end: + | If edge is incomplete and edge.next is not a part of speech: + | Apply PredictorRule to edge + | If edge is incomplete and edge.next is a part of speech: + | Apply ScannerRule to edge + | If edge is complete: + | Apply CompleterRule to edge + | Return any complete parses in the chart + """ + + def __init__( + self, + grammar, + strategy=BU_LC_INCREMENTAL_STRATEGY, + trace=0, + trace_chart_width=50, + chart_class=IncrementalChart, + ): + """ + Create a new Earley chart parser, that uses ``grammar`` to + parse texts. + + :type grammar: CFG + :param grammar: The grammar used to parse texts. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing + output. + :type trace_chart_width: int + :param trace_chart_width: The default total width reserved for + the chart in trace output. The remainder of each line will + be used to display edges. + :param chart_class: The class that should be used to create + the charts used by this parser. + """ + self._grammar = grammar + self._trace = trace + self._trace_chart_width = trace_chart_width + self._chart_class = chart_class + + self._axioms = [] + self._inference_rules = [] + for rule in strategy: + if rule.NUM_EDGES == 0: + self._axioms.append(rule) + elif rule.NUM_EDGES == 1: + self._inference_rules.append(rule) + else: + raise ValueError( + "Incremental inference rules must have " "NUM_EDGES == 0 or 1" + ) + + def chart_parse(self, tokens, trace=None): + if trace is None: + trace = self._trace + trace_new_edges = self._trace_new_edges + + tokens = list(tokens) + self._grammar.check_coverage(tokens) + chart = self._chart_class(tokens) + grammar = self._grammar + + # Width, for printing trace edges. + trace_edge_width = self._trace_chart_width // (chart.num_leaves() + 1) + if trace: + print(chart.pretty_format_leaves(trace_edge_width)) + + for axiom in self._axioms: + new_edges = list(axiom.apply(chart, grammar)) + trace_new_edges(chart, axiom, new_edges, trace, trace_edge_width) + + inference_rules = self._inference_rules + for end in range(chart.num_leaves() + 1): + if trace > 1: + print("\n* Processing queue:", end, "\n") + agenda = list(chart.select(end=end)) + while agenda: + edge = agenda.pop() + for rule in inference_rules: + new_edges = list(rule.apply(chart, grammar, edge)) + trace_new_edges(chart, rule, new_edges, trace, trace_edge_width) + for new_edge in new_edges: + if new_edge.end() == end: + agenda.append(new_edge) + + return chart + + +class EarleyChartParser(IncrementalChartParser): + def __init__(self, grammar, **parser_args): + IncrementalChartParser.__init__(self, grammar, EARLEY_STRATEGY, **parser_args) + + +class IncrementalTopDownChartParser(IncrementalChartParser): + def __init__(self, grammar, **parser_args): + IncrementalChartParser.__init__( + self, grammar, TD_INCREMENTAL_STRATEGY, **parser_args + ) + + +class IncrementalBottomUpChartParser(IncrementalChartParser): + def __init__(self, grammar, **parser_args): + IncrementalChartParser.__init__( + self, grammar, BU_INCREMENTAL_STRATEGY, **parser_args + ) + + +class IncrementalBottomUpLeftCornerChartParser(IncrementalChartParser): + def __init__(self, grammar, **parser_args): + IncrementalChartParser.__init__( + self, grammar, BU_LC_INCREMENTAL_STRATEGY, **parser_args + ) + + +class IncrementalLeftCornerChartParser(IncrementalChartParser): + def __init__(self, grammar, **parser_args): + if not grammar.is_nonempty(): + raise ValueError( + "IncrementalLeftCornerParser only works for grammars " + "without empty productions." + ) + IncrementalChartParser.__init__( + self, grammar, LC_INCREMENTAL_STRATEGY, **parser_args + ) + + +# //////////////////////////////////////////////////////////// +# Incremental FCFG Chart Parsers +# //////////////////////////////////////////////////////////// + +EARLEY_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureTopDownInitRule(), + FeatureCompleterRule(), + FeatureScannerRule(), + FeaturePredictorRule(), +] +TD_INCREMENTAL_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureTopDownInitRule(), + FeatureTopDownPredictRule(), + FeatureCompleteFundamentalRule(), +] +BU_INCREMENTAL_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureEmptyPredictRule(), + FeatureBottomUpPredictRule(), + FeatureCompleteFundamentalRule(), +] +BU_LC_INCREMENTAL_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureEmptyPredictRule(), + FeatureBottomUpPredictCombineRule(), + FeatureCompleteFundamentalRule(), +] + + +class FeatureIncrementalChartParser(IncrementalChartParser, FeatureChartParser): + def __init__( + self, + grammar, + strategy=BU_LC_INCREMENTAL_FEATURE_STRATEGY, + trace_chart_width=20, + chart_class=FeatureIncrementalChart, + **parser_args + ): + IncrementalChartParser.__init__( + self, + grammar, + strategy=strategy, + trace_chart_width=trace_chart_width, + chart_class=chart_class, + **parser_args + ) + + +class FeatureEarleyChartParser(FeatureIncrementalChartParser): + def __init__(self, grammar, **parser_args): + FeatureIncrementalChartParser.__init__( + self, grammar, EARLEY_FEATURE_STRATEGY, **parser_args + ) + + +class FeatureIncrementalTopDownChartParser(FeatureIncrementalChartParser): + def __init__(self, grammar, **parser_args): + FeatureIncrementalChartParser.__init__( + self, grammar, TD_INCREMENTAL_FEATURE_STRATEGY, **parser_args + ) + + +class FeatureIncrementalBottomUpChartParser(FeatureIncrementalChartParser): + def __init__(self, grammar, **parser_args): + FeatureIncrementalChartParser.__init__( + self, grammar, BU_INCREMENTAL_FEATURE_STRATEGY, **parser_args + ) + + +class FeatureIncrementalBottomUpLeftCornerChartParser(FeatureIncrementalChartParser): + def __init__(self, grammar, **parser_args): + FeatureIncrementalChartParser.__init__( + self, grammar, BU_LC_INCREMENTAL_FEATURE_STRATEGY, **parser_args + ) + + +# //////////////////////////////////////////////////////////// +# Demonstration +# //////////////////////////////////////////////////////////// + + +def demo( + print_times=True, + print_grammar=False, + print_trees=True, + trace=2, + sent="I saw John with a dog with my cookie", + numparses=5, +): + """ + A demonstration of the Earley parsers. + """ + import sys + import time + + from nltk.parse.chart import demo_grammar + + # The grammar for ChartParser and SteppingChartParser: + grammar = demo_grammar() + if print_grammar: + print("* Grammar") + print(grammar) + + # Tokenize the sample sentence. + print("* Sentence:") + print(sent) + tokens = sent.split() + print(tokens) + print() + + # Do the parsing. + earley = EarleyChartParser(grammar, trace=trace) + t = perf_counter() + chart = earley.chart_parse(tokens) + parses = list(chart.parses(grammar.start())) + t = perf_counter() - t + + # Print results. + if numparses: + assert len(parses) == numparses, "Not all parses found" + if print_trees: + for tree in parses: + print(tree) + else: + print("Nr trees:", len(parses)) + if print_times: + print("Time:", t) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/evaluate.py b/nltk/parse/evaluate.py new file mode 100644 index 0000000..75290b5 --- /dev/null +++ b/nltk/parse/evaluate.py @@ -0,0 +1,129 @@ +# Natural Language Toolkit: evaluation of dependency parser +# +# Author: Long Duong +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +import unicodedata + + +class DependencyEvaluator: + """ + Class for measuring labelled and unlabelled attachment score for + dependency parsing. Note that the evaluation ignores punctuation. + + >>> from nltk.parse import DependencyGraph, DependencyEvaluator + + >>> gold_sent = DependencyGraph(\""" + ... Pierre NNP 2 NMOD + ... Vinken NNP 8 SUB + ... , , 2 P + ... 61 CD 5 NMOD + ... years NNS 6 AMOD + ... old JJ 2 NMOD + ... , , 2 P + ... will MD 0 ROOT + ... join VB 8 VC + ... the DT 11 NMOD + ... board NN 9 OBJ + ... as IN 9 VMOD + ... a DT 15 NMOD + ... nonexecutive JJ 15 NMOD + ... director NN 12 PMOD + ... Nov. NNP 9 VMOD + ... 29 CD 16 NMOD + ... . . 9 VMOD + ... \""") + + >>> parsed_sent = DependencyGraph(\""" + ... Pierre NNP 8 NMOD + ... Vinken NNP 1 SUB + ... , , 3 P + ... 61 CD 6 NMOD + ... years NNS 6 AMOD + ... old JJ 2 NMOD + ... , , 3 AMOD + ... will MD 0 ROOT + ... join VB 8 VC + ... the DT 11 AMOD + ... board NN 9 OBJECT + ... as IN 9 NMOD + ... a DT 15 NMOD + ... nonexecutive JJ 15 NMOD + ... director NN 12 PMOD + ... Nov. NNP 9 VMOD + ... 29 CD 16 NMOD + ... . . 9 VMOD + ... \""") + + >>> de = DependencyEvaluator([parsed_sent],[gold_sent]) + >>> las, uas = de.eval() + >>> las + 0.6 + >>> uas + 0.8 + >>> abs(uas - 0.8) < 0.00001 + True + """ + + def __init__(self, parsed_sents, gold_sents): + """ + :param parsed_sents: the list of parsed_sents as the output of parser + :type parsed_sents: list(DependencyGraph) + """ + self._parsed_sents = parsed_sents + self._gold_sents = gold_sents + + def _remove_punct(self, inStr): + """ + Function to remove punctuation from Unicode string. + :param input: the input string + :return: Unicode string after remove all punctuation + """ + punc_cat = {"Pc", "Pd", "Ps", "Pe", "Pi", "Pf", "Po"} + return "".join(x for x in inStr if unicodedata.category(x) not in punc_cat) + + def eval(self): + """ + Return the Labeled Attachment Score (LAS) and Unlabeled Attachment Score (UAS) + + :return : tuple(float,float) + """ + if len(self._parsed_sents) != len(self._gold_sents): + raise ValueError( + " Number of parsed sentence is different with number of gold sentence." + ) + + corr = 0 + corrL = 0 + total = 0 + + for i in range(len(self._parsed_sents)): + parsed_sent_nodes = self._parsed_sents[i].nodes + gold_sent_nodes = self._gold_sents[i].nodes + + if len(parsed_sent_nodes) != len(gold_sent_nodes): + raise ValueError("Sentences must have equal length.") + + for parsed_node_address, parsed_node in parsed_sent_nodes.items(): + gold_node = gold_sent_nodes[parsed_node_address] + + if parsed_node["word"] is None: + continue + if parsed_node["word"] != gold_node["word"]: + raise ValueError("Sentence sequence is not matched.") + + # Ignore if word is punctuation by default + # if (parsed_sent[j]["word"] in string.punctuation): + if self._remove_punct(parsed_node["word"]) == "": + continue + + total += 1 + if parsed_node["head"] == gold_node["head"]: + corr += 1 + if parsed_node["rel"] == gold_node["rel"]: + corrL += 1 + + return corrL / total, corr / total diff --git a/nltk/parse/featurechart.py b/nltk/parse/featurechart.py new file mode 100644 index 0000000..c504997 --- /dev/null +++ b/nltk/parse/featurechart.py @@ -0,0 +1,674 @@ +# Natural Language Toolkit: Chart Parser for Feature-Based Grammars +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Rob Speer +# Peter Ljunglöf +# URL: +# For license information, see LICENSE.TXT + +""" +Extension of chart parsing implementation to handle grammars with +feature structures as nodes. +""" +from time import perf_counter + +from nltk.featstruct import TYPE, FeatStruct, find_variables, unify +from nltk.grammar import ( + CFG, + FeatStructNonterminal, + Nonterminal, + Production, + is_nonterminal, + is_terminal, +) +from nltk.parse.chart import ( + BottomUpPredictCombineRule, + BottomUpPredictRule, + CachedTopDownPredictRule, + Chart, + ChartParser, + EdgeI, + EmptyPredictRule, + FundamentalRule, + LeafInitRule, + SingleEdgeFundamentalRule, + TopDownInitRule, + TreeEdge, +) +from nltk.sem import logic +from nltk.tree import Tree + +# //////////////////////////////////////////////////////////// +# Tree Edge +# //////////////////////////////////////////////////////////// + + +class FeatureTreeEdge(TreeEdge): + """ + A specialized tree edge that allows shared variable bindings + between nonterminals on the left-hand side and right-hand side. + + Each ``FeatureTreeEdge`` contains a set of ``bindings``, i.e., a + dictionary mapping from variables to values. If the edge is not + complete, then these bindings are simply stored. However, if the + edge is complete, then the constructor applies these bindings to + every nonterminal in the edge whose symbol implements the + interface ``SubstituteBindingsI``. + """ + + def __init__(self, span, lhs, rhs, dot=0, bindings=None): + """ + Construct a new edge. If the edge is incomplete (i.e., if + ``dot alpha \* B1 beta][i:j]`` + - ``[B2 -> gamma \*][j:k]`` + + licenses the edge: + + - ``[A -> alpha B3 \* beta][i:j]`` + + assuming that B1 and B2 can be unified to generate B3. + """ + + def apply(self, chart, grammar, left_edge, right_edge): + # Make sure the rule is applicable. + if not ( + left_edge.end() == right_edge.start() + and left_edge.is_incomplete() + and right_edge.is_complete() + and isinstance(left_edge, FeatureTreeEdge) + ): + return + found = right_edge.lhs() + nextsym = left_edge.nextsym() + if isinstance(right_edge, FeatureTreeEdge): + if not is_nonterminal(nextsym): + return + if left_edge.nextsym()[TYPE] != right_edge.lhs()[TYPE]: + return + # Create a copy of the bindings. + bindings = left_edge.bindings() + # We rename vars here, because we don't want variables + # from the two different productions to match. + found = found.rename_variables(used_vars=left_edge.variables()) + # Unify B1 (left_edge.nextsym) with B2 (right_edge.lhs) to + # generate B3 (result). + result = unify(nextsym, found, bindings, rename_vars=False) + if result is None: + return + else: + if nextsym != found: + return + # Create a copy of the bindings. + bindings = left_edge.bindings() + + # Construct the new edge. + new_edge = left_edge.move_dot_forward(right_edge.end(), bindings) + + # Add it to the chart, with appropriate child pointers. + if chart.insert_with_backpointer(new_edge, left_edge, right_edge): + yield new_edge + + +class FeatureSingleEdgeFundamentalRule(SingleEdgeFundamentalRule): + """ + A specialized version of the completer / single edge fundamental rule + that operates on nonterminals whose symbols are ``FeatStructNonterminal``. + Rather than simply comparing the nonterminals for equality, they are + unified. + """ + + _fundamental_rule = FeatureFundamentalRule() + + def _apply_complete(self, chart, grammar, right_edge): + fr = self._fundamental_rule + for left_edge in chart.select( + end=right_edge.start(), is_complete=False, nextsym=right_edge.lhs() + ): + yield from fr.apply(chart, grammar, left_edge, right_edge) + + def _apply_incomplete(self, chart, grammar, left_edge): + fr = self._fundamental_rule + for right_edge in chart.select( + start=left_edge.end(), is_complete=True, lhs=left_edge.nextsym() + ): + yield from fr.apply(chart, grammar, left_edge, right_edge) + + +# //////////////////////////////////////////////////////////// +# Top-Down Prediction +# //////////////////////////////////////////////////////////// + + +class FeatureTopDownInitRule(TopDownInitRule): + def apply(self, chart, grammar): + for prod in grammar.productions(lhs=grammar.start()): + new_edge = FeatureTreeEdge.from_production(prod, 0) + if chart.insert(new_edge, ()): + yield new_edge + + +class FeatureTopDownPredictRule(CachedTopDownPredictRule): + r""" + A specialized version of the (cached) top down predict rule that operates + on nonterminals whose symbols are ``FeatStructNonterminal``. Rather + than simply comparing the nonterminals for equality, they are + unified. + + The top down expand rule states that: + + - ``[A -> alpha \* B1 beta][i:j]`` + + licenses the edge: + + - ``[B2 -> \* gamma][j:j]`` + + for each grammar production ``B2 -> gamma``, assuming that B1 + and B2 can be unified. + """ + + def apply(self, chart, grammar, edge): + if edge.is_complete(): + return + nextsym, index = edge.nextsym(), edge.end() + if not is_nonterminal(nextsym): + return + + # If we've already applied this rule to an edge with the same + # next & end, and the chart & grammar have not changed, then + # just return (no new edges to add). + nextsym_with_bindings = edge.next_with_bindings() + done = self._done.get((nextsym_with_bindings, index), (None, None)) + if done[0] is chart and done[1] is grammar: + return + + for prod in grammar.productions(lhs=nextsym): + # If the left corner in the predicted production is + # leaf, it must match with the input. + if prod.rhs(): + first = prod.rhs()[0] + if is_terminal(first): + if index >= chart.num_leaves(): + continue + if first != chart.leaf(index): + continue + + # We rename vars here, because we don't want variables + # from the two different productions to match. + if unify(prod.lhs(), nextsym_with_bindings, rename_vars=True): + new_edge = FeatureTreeEdge.from_production(prod, edge.end()) + if chart.insert(new_edge, ()): + yield new_edge + + # Record the fact that we've applied this rule. + self._done[nextsym_with_bindings, index] = (chart, grammar) + + +# //////////////////////////////////////////////////////////// +# Bottom-Up Prediction +# //////////////////////////////////////////////////////////// + + +class FeatureBottomUpPredictRule(BottomUpPredictRule): + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + return + for prod in grammar.productions(rhs=edge.lhs()): + if isinstance(edge, FeatureTreeEdge): + _next = prod.rhs()[0] + if not is_nonterminal(_next): + continue + + new_edge = FeatureTreeEdge.from_production(prod, edge.start()) + if chart.insert(new_edge, ()): + yield new_edge + + +class FeatureBottomUpPredictCombineRule(BottomUpPredictCombineRule): + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + return + found = edge.lhs() + for prod in grammar.productions(rhs=found): + bindings = {} + if isinstance(edge, FeatureTreeEdge): + _next = prod.rhs()[0] + if not is_nonterminal(_next): + continue + + # We rename vars here, because we don't want variables + # from the two different productions to match. + used_vars = find_variables( + (prod.lhs(),) + prod.rhs(), fs_class=FeatStruct + ) + found = found.rename_variables(used_vars=used_vars) + + result = unify(_next, found, bindings, rename_vars=False) + if result is None: + continue + + new_edge = FeatureTreeEdge.from_production( + prod, edge.start() + ).move_dot_forward(edge.end(), bindings) + if chart.insert(new_edge, (edge,)): + yield new_edge + + +class FeatureEmptyPredictRule(EmptyPredictRule): + def apply(self, chart, grammar): + for prod in grammar.productions(empty=True): + for index in range(chart.num_leaves() + 1): + new_edge = FeatureTreeEdge.from_production(prod, index) + if chart.insert(new_edge, ()): + yield new_edge + + +# //////////////////////////////////////////////////////////// +# Feature Chart Parser +# //////////////////////////////////////////////////////////// + +TD_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureTopDownInitRule(), + FeatureTopDownPredictRule(), + FeatureSingleEdgeFundamentalRule(), +] +BU_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureEmptyPredictRule(), + FeatureBottomUpPredictRule(), + FeatureSingleEdgeFundamentalRule(), +] +BU_LC_FEATURE_STRATEGY = [ + LeafInitRule(), + FeatureEmptyPredictRule(), + FeatureBottomUpPredictCombineRule(), + FeatureSingleEdgeFundamentalRule(), +] + + +class FeatureChartParser(ChartParser): + def __init__( + self, + grammar, + strategy=BU_LC_FEATURE_STRATEGY, + trace_chart_width=20, + chart_class=FeatureChart, + **parser_args, + ): + ChartParser.__init__( + self, + grammar, + strategy=strategy, + trace_chart_width=trace_chart_width, + chart_class=chart_class, + **parser_args, + ) + + +class FeatureTopDownChartParser(FeatureChartParser): + def __init__(self, grammar, **parser_args): + FeatureChartParser.__init__(self, grammar, TD_FEATURE_STRATEGY, **parser_args) + + +class FeatureBottomUpChartParser(FeatureChartParser): + def __init__(self, grammar, **parser_args): + FeatureChartParser.__init__(self, grammar, BU_FEATURE_STRATEGY, **parser_args) + + +class FeatureBottomUpLeftCornerChartParser(FeatureChartParser): + def __init__(self, grammar, **parser_args): + FeatureChartParser.__init__( + self, grammar, BU_LC_FEATURE_STRATEGY, **parser_args + ) + + +# //////////////////////////////////////////////////////////// +# Instantiate Variable Chart +# //////////////////////////////////////////////////////////// + + +class InstantiateVarsChart(FeatureChart): + """ + A specialized chart that 'instantiates' variables whose names + start with '@', by replacing them with unique new variables. + In particular, whenever a complete edge is added to the chart, any + variables in the edge's ``lhs`` whose names start with '@' will be + replaced by unique new ``Variable``. + """ + + def __init__(self, tokens): + FeatureChart.__init__(self, tokens) + + def initialize(self): + self._instantiated = set() + FeatureChart.initialize(self) + + def insert(self, edge, child_pointer_list): + if edge in self._instantiated: + return False + self.instantiate_edge(edge) + return FeatureChart.insert(self, edge, child_pointer_list) + + def instantiate_edge(self, edge): + """ + If the edge is a ``FeatureTreeEdge``, and it is complete, + then instantiate all variables whose names start with '@', + by replacing them with unique new variables. + + Note that instantiation is done in-place, since the + parsing algorithms might already hold a reference to + the edge for future use. + """ + # If the edge is a leaf, or is not complete, or is + # already in the chart, then just return it as-is. + if not isinstance(edge, FeatureTreeEdge): + return + if not edge.is_complete(): + return + if edge in self._edge_to_cpls: + return + + # Get a list of variables that need to be instantiated. + # If there are none, then return as-is. + inst_vars = self.inst_vars(edge) + if not inst_vars: + return + + # Instantiate the edge! + self._instantiated.add(edge) + edge._lhs = edge.lhs().substitute_bindings(inst_vars) + + def inst_vars(self, edge): + return { + var: logic.unique_variable() + for var in edge.lhs().variables() + if var.name.startswith("@") + } + + +# //////////////////////////////////////////////////////////// +# Demo +# //////////////////////////////////////////////////////////// + + +def demo_grammar(): + from nltk.grammar import FeatureGrammar + + return FeatureGrammar.fromstring( + """ +S -> NP VP +PP -> Prep NP +NP -> NP PP +VP -> VP PP +VP -> Verb NP +VP -> Verb +NP -> Det[pl=?x] Noun[pl=?x] +NP -> "John" +NP -> "I" +Det -> "the" +Det -> "my" +Det[-pl] -> "a" +Noun[-pl] -> "dog" +Noun[-pl] -> "cookie" +Verb -> "ate" +Verb -> "saw" +Prep -> "with" +Prep -> "under" +""" + ) + + +def demo( + print_times=True, + print_grammar=True, + print_trees=True, + print_sentence=True, + trace=1, + parser=FeatureChartParser, + sent="I saw John with a dog with my cookie", +): + import sys + import time + + print() + grammar = demo_grammar() + if print_grammar: + print(grammar) + print() + print("*", parser.__name__) + if print_sentence: + print("Sentence:", sent) + tokens = sent.split() + t = perf_counter() + cp = parser(grammar, trace=trace) + chart = cp.chart_parse(tokens) + trees = list(chart.parses(grammar.start())) + if print_times: + print("Time: %s" % (perf_counter() - t)) + if print_trees: + for tree in trees: + print(tree) + else: + print("Nr trees:", len(trees)) + + +def run_profile(): + import profile + + profile.run("for i in range(1): demo()", "/tmp/profile.out") + import pstats + + p = pstats.Stats("/tmp/profile.out") + p.strip_dirs().sort_stats("time", "cum").print_stats(60) + p.strip_dirs().sort_stats("cum", "time").print_stats(60) + + +if __name__ == "__main__": + from nltk.data import load + + demo() + print() + grammar = load("grammars/book_grammars/feat0.fcfg") + cp = FeatureChartParser(grammar, trace=2) + sent = "Kim likes children" + tokens = sent.split() + trees = cp.parse(tokens) + for tree in trees: + print(tree) diff --git a/nltk/parse/generate.py b/nltk/parse/generate.py new file mode 100644 index 0000000..e2e6936 --- /dev/null +++ b/nltk/parse/generate.py @@ -0,0 +1,132 @@ +# Natural Language Toolkit: Generating from a CFG +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Peter Ljunglöf +# Eric Kafe +# URL: +# For license information, see LICENSE.TXT +# + +import itertools +import sys + +from nltk.grammar import Nonterminal + +#: Upper bound on the number of derivation-expansion steps a single +#: ``generate`` call may perform. A recursive grammar can derive an exponential +#: (doubly-exponential for a self-embedding rule) number of sentences, so with +#: the default ``depth`` and no ``n`` limit a tiny grammar (e.g. ``S -> S S | +#: 'a'``) makes generation hang or exhaust memory (CWE-400). Once this many +#: expansion steps have been taken, generation raises ``ValueError`` instead of +#: running unbounded. Raise it if you legitimately need to enumerate a larger +#: (terminating) grammar. +MAX_GENERATE_OPERATIONS = 1_000_000 + + +class _GenerationBudget: + """Counts derivation-expansion steps and aborts runaway generation. + + The whole enumeration shares one budget, so it bounds total work however + the caller consumes the iterator (one ``next()`` or a full ``list()``). + """ + + __slots__ = ("remaining", "limit") + + def __init__(self, limit): + self.remaining = limit + self.limit = limit + + def spend(self): + self.remaining -= 1 + if self.remaining < 0: + raise ValueError( + "Refusing to generate further: a recursive grammar can derive " + "an exponential number of sentences, and generation exceeded " + "the limit of %d derivation-expansion steps (CWE-400). Pass a " + "smaller 'depth' or 'n', remove cyclic/self-embedding rules, or " + "raise nltk.parse.generate.MAX_GENERATE_OPERATIONS." % self.limit + ) + + +def generate(grammar, start=None, depth=None, n=None): + """ + Generates an iterator of all sentences from a CFG. + + :param grammar: The Grammar used to generate sentences. + :param start: The Nonterminal from which to start generate sentences. + :param depth: The maximal depth of the generated tree. + :param n: The maximum number of sentences to return. + :return: An iterator of lists of terminal tokens. + :raise ValueError: if generation exceeds ``MAX_GENERATE_OPERATIONS`` + derivation-expansion steps, which a recursive grammar reaches with + the default ``depth`` and no ``n`` limit (CWE-400). + """ + if not start: + start = grammar.start() + if depth is None: + # Safe default, assuming the grammar may be recursive: + depth = (sys.getrecursionlimit() // 3) - 3 + + budget = _GenerationBudget(MAX_GENERATE_OPERATIONS) + iter = _generate_all(grammar, [start], depth, budget) + + # ``n is not None`` (not ``if n``) so that n=0 is honoured as "return no + # sentences" rather than being treated as "no limit". + if n is not None: + iter = itertools.islice(iter, n) + + return iter + + +def _generate_all(grammar, items, depth, budget): + if items: + try: + for frag1 in _generate_one(grammar, items[0], depth, budget): + for frag2 in _generate_all(grammar, items[1:], depth, budget): + yield frag1 + frag2 + except RecursionError as error: + # Helpful error message while still showing the recursion stack. + raise RuntimeError( + "The grammar has rule(s) that yield infinite recursion!\n\ +Eventually use a lower 'depth', or a higher 'sys.setrecursionlimit()'." + ) from error + else: + yield [] + + +def _generate_one(grammar, item, depth, budget): + # Count every expansion so a recursive grammar can't enumerate an + # unbounded/exponential number of derivations (CWE-400). + budget.spend() + if depth > 0: + if isinstance(item, Nonterminal): + for prod in grammar.productions(lhs=item): + yield from _generate_all(grammar, prod.rhs(), depth - 1, budget) + else: + yield [item] + + +demo_grammar = """ + S -> NP VP + NP -> Det N + PP -> P NP + VP -> 'slept' | 'saw' NP | 'walked' PP + Det -> 'the' | 'a' + N -> 'man' | 'park' | 'dog' + P -> 'in' | 'with' +""" + + +def demo(N=23): + from nltk.grammar import CFG + + print("Generating the first %d sentences for demo grammar:" % (N,)) + print(demo_grammar) + grammar = CFG.fromstring(demo_grammar) + for n, sent in enumerate(generate(grammar, n=N), 1): + print("%3d. %s" % (n, " ".join(sent))) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/malt.py b/nltk/parse/malt.py new file mode 100644 index 0000000..f5d4565 --- /dev/null +++ b/nltk/parse/malt.py @@ -0,0 +1,407 @@ +# Natural Language Toolkit: Interface to MaltParser +# +# Author: Dan Garrette +# Contributor: Liling Tan, Mustufain, osamamukhtar11 +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +import inspect +import os +import subprocess +import sys +import tempfile + +from nltk.data import ZipFilePathPointer +from nltk.internals import find_dir, find_file, find_jars_within_path +from nltk.parse.api import ParserI +from nltk.parse.dependencygraph import DependencyGraph +from nltk.parse.util import taggedsents_to_conll + + +def malt_regex_tagger(): + from nltk.tag import RegexpTagger + + _tagger = RegexpTagger( + [ + (r"\.$", "."), + (r"\,$", ","), + (r"\?$", "?"), # fullstop, comma, Qmark + (r"\($", "("), + (r"\)$", ")"), # round brackets + (r"\[$", "["), + (r"\]$", "]"), # square brackets + (r"^-?[0-9]+(\.[0-9]+)?$", "CD"), # cardinal numbers + (r"(The|the|A|a|An|an)$", "DT"), # articles + (r"(He|he|She|she|It|it|I|me|Me|You|you)$", "PRP"), # pronouns + (r"(His|his|Her|her|Its|its)$", "PRP$"), # possessive + (r"(my|Your|your|Yours|yours)$", "PRP$"), # possessive + (r"(on|On|in|In|at|At|since|Since)$", "IN"), # time prepopsitions + (r"(for|For|ago|Ago|before|Before)$", "IN"), # time prepopsitions + (r"(till|Till|until|Until)$", "IN"), # time prepopsitions + (r"(by|By|beside|Beside)$", "IN"), # space prepopsitions + (r"(under|Under|below|Below)$", "IN"), # space prepopsitions + (r"(over|Over|above|Above)$", "IN"), # space prepopsitions + (r"(across|Across|through|Through)$", "IN"), # space prepopsitions + (r"(into|Into|towards|Towards)$", "IN"), # space prepopsitions + (r"(onto|Onto|from|From)$", "IN"), # space prepopsitions + (r".*able$", "JJ"), # adjectives + (r".*ness$", "NN"), # nouns formed from adjectives + (r".*ly$", "RB"), # adverbs + (r".*s$", "NNS"), # plural nouns + (r".*ing$", "VBG"), # gerunds + (r".*ed$", "VBD"), # past tense verbs + (r".*", "NN"), # nouns (default) + ] + ) + return _tagger.tag + + +def find_maltparser(parser_dirname): + """ + A module to find MaltParser .jar file and its dependencies. + """ + # Accept str or os.PathLike uniformly (find_dir() requires a str). + parser_dirname = os.fspath(parser_dirname) + # Only accept an explicit *absolute* directory as-is. A relative name must + # not be resolved against the current working directory: an attacker able to + # write to the CWD could otherwise plant a ``maltparser-*/`` directory there + # and have its jars placed on the Java classpath (and its ``org.maltparser`` + # main class executed), overriding a trusted ``MALT_PARSER`` -- an untrusted + # search path (CWE-426). A relative name is resolved through ``MALT_PARSER``. + if os.path.isabs(parser_dirname) and os.path.isdir(parser_dirname): + _malt_dir = parser_dirname + else: # Try to find path to maltparser directory in environment variables. + _malt_dir = find_dir(parser_dirname, env_vars=("MALT_PARSER",)) + # Checks that that the found directory contains all the necessary .jar + malt_dependencies = ["", "", ""] + _malt_jars = set(find_jars_within_path(_malt_dir)) + _jars = {os.path.split(jar)[1] for jar in _malt_jars} + malt_dependencies = {"log4j.jar", "libsvm.jar", "liblinear-1.8.jar"} + + assert malt_dependencies.issubset(_jars) + assert any( + filter(lambda i: i.startswith("maltparser-") and i.endswith(".jar"), _jars) + ) + return list(_malt_jars) + + +def find_malt_model(model_filename): + """ + A module to find pre-trained MaltParser model. + """ + if model_filename is None: + return "malt_temp.mco" + elif os.path.exists(model_filename): # If a full path is given. + return model_filename + else: # Try to find path to malt model in environment variables. + return find_file(model_filename, env_vars=("MALT_MODEL",), verbose=False) + + +class MaltParser(ParserI): + """ + A class for dependency parsing with MaltParser. The input is the paths to: + - (optionally) a maltparser directory + - (optionally) the path to a pre-trained MaltParser .mco model file + - (optionally) the tagger to use for POS tagging before parsing + - (optionally) additional Java arguments + + Example: + >>> from nltk.parse import malt + >>> # With MALT_PARSER and MALT_MODEL environment set. + >>> mp = malt.MaltParser(model_filename='engmalt.linear-1.7.mco') # doctest: +SKIP + >>> mp.parse_one('I shot an elephant in my pajamas .'.split()).tree() # doctest: +SKIP + (shot I (elephant an) (in (pajamas my)) .) + >>> # Without MALT_PARSER and MALT_MODEL environment. + >>> mp = malt.MaltParser('/home/user/maltparser-1.9.2/', '/home/user/engmalt.linear-1.7.mco') # doctest: +SKIP + >>> mp.parse_one('I shot an elephant in my pajamas .'.split()).tree() # doctest: +SKIP + (shot I (elephant an) (in (pajamas my)) .) + """ + + def __init__( + self, + parser_dirname="", + model_filename=None, + tagger=None, + additional_java_args=None, + ): + """ + An interface for parsing with the Malt Parser. + + :param parser_dirname: The path to the maltparser directory that + contains the maltparser-1.x.jar + :type parser_dirname: str + :param model_filename: The name of the pre-trained model with .mco file + extension. If provided, training will not be required. + (see http://www.maltparser.org/mco/mco.html and + see http://www.patful.com/chalk/node/185) + :type model_filename: str + :param tagger: The tagger used to POS tag the raw string before + formatting to CONLL format. It should behave like `nltk.pos_tag` + :type tagger: function + :param additional_java_args: This is the additional Java arguments that + one can use when calling Maltparser, usually this is the heapsize + limits, e.g. `additional_java_args=['-Xmx1024m']` + (see https://javarevisited.blogspot.com/2011/05/java-heap-space-memory-size-jvm.html) + :type additional_java_args: list + """ + + # Find all the necessary jar files for MaltParser. + self.malt_jars = find_maltparser(parser_dirname) + # Initialize additional java arguments. + self.additional_java_args = ( + additional_java_args if additional_java_args is not None else [] + ) + # Initialize model. + self.model = find_malt_model(model_filename) + self._trained = self.model != "malt_temp.mco" + # Set the working_dir parameters i.e. `-w` from MaltParser's option. + self.working_dir = tempfile.gettempdir() + # Initialize POS tagger. + self.tagger = tagger if tagger is not None else malt_regex_tagger() + + def parse_tagged_sents(self, sentences, verbose=False, top_relation_label="null"): + """ + Use MaltParser to parse multiple POS tagged sentences. Takes multiple + sentences where each sentence is a list of (word, tag) tuples. + The sentences must have already been tokenized and tagged. + + :param sentences: Input sentences to parse + :type sentence: list(list(tuple(str, str))) + :return: iter(iter(``DependencyGraph``)) the dependency graph + representation of each sentence + """ + if not self._trained: + raise Exception("Parser has not been trained. Call train() first.") + + input_file_name = None + output_file_name = None + try: + with tempfile.NamedTemporaryFile( + prefix="malt_input.conll.", dir=self.working_dir, mode="w", delete=False + ) as input_file, tempfile.NamedTemporaryFile( + prefix="malt_output.conll.", + dir=self.working_dir, + mode="w", + delete=False, + ) as output_file: + input_file_name = input_file.name + output_file_name = output_file.name + + model_dir = os.path.split(self.model)[0] + model_cwd = os.path.abspath(model_dir) if model_dir else None + if model_cwd and not os.path.isdir(model_cwd): + model_cwd = None + + # Convert list of sentences to CONLL format. + for line in taggedsents_to_conll(sentences): + input_file.write(str(line)) + input_file.close() + + # Generate command to run maltparser. + cmd = self.generate_malt_command( + input_file_name, output_file_name, mode="parse" + ) + + # MaltParser needs to run from the model directory; use + # subprocess cwd so other threads do not see a process-wide chdir. + ret = self._execute(cmd, verbose, cwd=model_cwd) # Run command. + + if ret != 0: + raise Exception( + "MaltParser parsing (%s) failed with exit " + "code %d" % (" ".join(cmd), ret) + ) + + # Must return iter(iter(Tree)) + with open(output_file_name) as infile: + for tree_str in infile.read().split("\n\n"): + yield ( + iter( + [ + DependencyGraph( + tree_str, + top_relation_label=top_relation_label, + ) + ] + ) + ) + finally: + for filename in (input_file_name, output_file_name): + if filename: + try: + os.remove(filename) + except OSError: + pass + + def parse_sents(self, sentences, verbose=False, top_relation_label="null"): + """ + Use MaltParser to parse multiple sentences. + Takes a list of sentences, where each sentence is a list of words. + Each sentence will be automatically tagged with this + MaltParser instance's tagger. + + :param sentences: Input sentences to parse + :type sentence: list(list(str)) + :return: iter(DependencyGraph) + """ + tagged_sentences = (self.tagger(sentence) for sentence in sentences) + return self.parse_tagged_sents( + tagged_sentences, verbose, top_relation_label=top_relation_label + ) + + def generate_malt_command(self, inputfilename, outputfilename=None, mode=None): + """ + This function generates the maltparser command use at the terminal. + + :param inputfilename: path to the input file + :type inputfilename: str + :param outputfilename: path to the output file + :type outputfilename: str + """ + + cmd = ["java"] + cmd += self.additional_java_args # Adds additional java arguments + cmd += [ + "-cp", + os.pathsep.join(self.malt_jars), + ] # Adds classpaths for jars + cmd += ["org.maltparser.Malt"] # Adds the main function. + + # Adds the model file. + if os.path.exists(self.model): # when parsing + cmd += ["-c", os.path.split(self.model)[-1]] + else: # when learning + cmd += ["-c", self.model] + + cmd += ["-i", inputfilename] + if mode == "parse": + cmd += ["-o", outputfilename] + cmd += ["-m", mode] # mode use to generate parses. + return cmd + + @staticmethod + def _execute(cmd, verbose=False, cwd=None): + output = None if verbose else subprocess.PIPE + p = subprocess.Popen(cmd, stdout=output, stderr=output, cwd=cwd) + return p.wait() + + def train(self, depgraphs, verbose=False): + """ + Train MaltParser from a list of ``DependencyGraph`` objects + + :param depgraphs: list of ``DependencyGraph`` objects for training input data + :type depgraphs: DependencyGraph + """ + + # Write the conll_str to malt_train.conll file in /tmp/ + with tempfile.NamedTemporaryFile( + prefix="malt_train.conll.", dir=self.working_dir, mode="w", delete=False + ) as input_file: + input_str = "\n".join(dg.to_conll(10) for dg in depgraphs) + input_file.write(str(input_str)) + # Trains the model with the malt_train.conll + self.train_from_file(input_file.name, verbose=verbose) + # Removes the malt_train.conll once training finishes. + os.remove(input_file.name) + + def train_from_file(self, conll_file, verbose=False): + """ + Train MaltParser from a file + :param conll_file: str for the filename of the training input data + :type conll_file: str + """ + + # If conll_file is a ZipFilePathPointer, + # then we need to do some extra massaging + if isinstance(conll_file, ZipFilePathPointer): + with tempfile.NamedTemporaryFile( + prefix="malt_train.conll.", dir=self.working_dir, mode="w", delete=False + ) as input_file: + with conll_file.open() as conll_input_file: + conll_str = conll_input_file.read() + input_file.write(str(conll_str)) + return self.train_from_file(input_file.name, verbose=verbose) + + # Generate command to run maltparser. + cmd = self.generate_malt_command(conll_file, mode="learn") + ret = self._execute(cmd, verbose) + if ret != 0: + raise Exception( + "MaltParser training (%s) failed with exit " + "code %d" % (" ".join(cmd), ret) + ) + self._trained = True + + +if __name__ == "__main__": + """ + A demonstration function to show how NLTK users can use the malt parser API. + + >>> from nltk import pos_tag + >>> assert 'MALT_PARSER' in os.environ, str( + ... "Please set MALT_PARSER in your global environment, e.g.:\n" + ... "$ export MALT_PARSER='/home/user/maltparser-1.9.2/'") + >>> + >>> assert 'MALT_MODEL' in os.environ, str( + ... "Please set MALT_MODEL in your global environment, e.g.:\n" + ... "$ export MALT_MODEL='/home/user/engmalt.linear-1.7.mco'") + >>> + >>> _dg1_str = str("1 John _ NNP _ _ 2 SUBJ _ _\n" + ... "2 sees _ VB _ _ 0 ROOT _ _\n" + ... "3 a _ DT _ _ 4 SPEC _ _\n" + ... "4 dog _ NN _ _ 2 OBJ _ _\n" + ... "5 . _ . _ _ 2 PUNCT _ _\n") + >>> + >>> + >>> _dg2_str = str("1 John _ NNP _ _ 2 SUBJ _ _\n" + ... "2 walks _ VB _ _ 0 ROOT _ _\n" + ... "3 . _ . _ _ 2 PUNCT _ _\n") + >>> dg1 = DependencyGraph(_dg1_str) + >>> dg2 = DependencyGraph(_dg2_str) + >>> # Initialize a MaltParser object + >>> mp = MaltParser() + >>> + >>> # Trains a model. + >>> mp.train([dg1,dg2], verbose=False) + >>> sent1 = ['John','sees','Mary', '.'] + >>> sent2 = ['John', 'walks', 'a', 'dog', '.'] + >>> + >>> # Parse a single sentence. + >>> parsed_sent1 = mp.parse_one(sent1) + >>> parsed_sent2 = mp.parse_one(sent2) + >>> print(parsed_sent1.tree()) + (sees John Mary .) + >>> print(parsed_sent2.tree()) + (walks John (dog a) .) + >>> + >>> # Parsing multiple sentences. + >>> sentences = [sent1,sent2] + >>> parsed_sents = mp.parse_sents(sentences) + >>> print(next(next(parsed_sents)).tree()) + (sees John Mary .) + >>> print(next(next(parsed_sents)).tree()) + (walks John (dog a) .) + >>> + >>> # Initialize a MaltParser object with an English pre-trained model. + >>> parser_dirname = 'maltparser-1.9.2' + >>> model_name = 'engmalt.linear-1.7.mco' + >>> mp = MaltParser(parser_dirname=parser_dirname, model_filename=model_name, tagger=pos_tag) + >>> sent1 = 'I shot an elephant in my pajamas .'.split() + >>> sent2 = 'Time flies like banana .'.split() + >>> # Parse a single sentence. + >>> print(mp.parse_one(sent1).tree()) + (shot I (elephant an) (in (pajamas my)) .) + # Parsing multiple sentences + >>> sentences = [sent1,sent2] + >>> parsed_sents = mp.parse_sents(sentences) + >>> print(next(next(parsed_sents)).tree()) + (shot I (elephant an) (in (pajamas my)) .) + >>> print(next(next(parsed_sents)).tree()) + (flies Time (like banana) .) + """ + + import doctest + + doctest.testmod() diff --git a/nltk/parse/nonprojectivedependencyparser.py b/nltk/parse/nonprojectivedependencyparser.py new file mode 100644 index 0000000..70d81a5 --- /dev/null +++ b/nltk/parse/nonprojectivedependencyparser.py @@ -0,0 +1,772 @@ +# Natural Language Toolkit: Dependency Grammars +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Jason Narad +# +# URL: +# For license information, see LICENSE.TXT +# + +import logging +import math + +from nltk.parse.dependencygraph import DependencyGraph + +logger = logging.getLogger(__name__) + +################################################################# +# DependencyScorerI - Interface for Graph-Edge Weight Calculation +################################################################# + + +class DependencyScorerI: + """ + A scorer for calculated the weights on the edges of a weighted + dependency graph. This is used by a + ``ProbabilisticNonprojectiveParser`` to initialize the edge + weights of a ``DependencyGraph``. While typically this would be done + by training a binary classifier, any class that can return a + multidimensional list representation of the edge weights can + implement this interface. As such, it has no necessary + fields. + """ + + def __init__(self): + if self.__class__ == DependencyScorerI: + raise TypeError("DependencyScorerI is an abstract interface") + + def train(self, graphs): + """ + :type graphs: list(DependencyGraph) + :param graphs: A list of dependency graphs to train the scorer. + Typically the edges present in the graphs can be used as + positive training examples, and the edges not present as negative + examples. + """ + raise NotImplementedError() + + def score(self, graph): + """ + :type graph: DependencyGraph + :param graph: A dependency graph whose set of edges need to be + scored. + :rtype: A three-dimensional list of numbers. + :return: The score is returned in a multidimensional(3) list, such + that the outer-dimension refers to the head, and the + inner-dimension refers to the dependencies. For instance, + scores[0][1] would reference the list of scores corresponding to + arcs from node 0 to node 1. The node's 'address' field can be used + to determine its number identification. + + For further illustration, a score list corresponding to Fig.2 of + Keith Hall's 'K-best Spanning Tree Parsing' paper:: + + scores = [[[], [5], [1], [1]], + [[], [], [11], [4]], + [[], [10], [], [5]], + [[], [8], [8], []]] + + When used in conjunction with a MaxEntClassifier, each score would + correspond to the confidence of a particular edge being classified + with the positive training examples. + """ + raise NotImplementedError() + + +################################################################# +# NaiveBayesDependencyScorer +################################################################# + + +class NaiveBayesDependencyScorer(DependencyScorerI): + """ + A dependency scorer built around a MaxEnt classifier. In this + particular class that classifier is a ``NaiveBayesClassifier``. + It uses head-word, head-tag, child-word, and child-tag features + for classification. + + >>> from nltk.parse.dependencygraph import DependencyGraph, conll_data2 + + >>> graphs = [DependencyGraph(entry) for entry in conll_data2.split('\\n\\n') if entry] + >>> npp = ProbabilisticNonprojectiveParser() + >>> npp.train(graphs, NaiveBayesDependencyScorer()) + >>> parses = npp.parse(['Cathy', 'zag', 'hen', 'zwaaien', '.'], ['N', 'V', 'Pron', 'Adj', 'N', 'Punc']) + >>> len(list(parses)) + 1 + + """ + + def __init__(self): + pass # Do nothing without throwing error + + def train(self, graphs): + """ + Trains a ``NaiveBayesClassifier`` using the edges present in + graphs list as positive examples, the edges not present as + negative examples. Uses a feature vector of head-word, + head-tag, child-word, and child-tag. + + :type graphs: list(DependencyGraph) + :param graphs: A list of dependency graphs to train the scorer. + """ + + from nltk.classify import NaiveBayesClassifier + + # Create training labeled training examples + labeled_examples = [] + for graph in graphs: + for head_node in graph.nodes.values(): + for child_index, child_node in graph.nodes.items(): + if child_index in head_node["deps"]: + label = "T" + else: + label = "F" + labeled_examples.append( + ( + dict( + a=head_node["word"], + b=head_node["tag"], + c=child_node["word"], + d=child_node["tag"], + ), + label, + ) + ) + + self.classifier = NaiveBayesClassifier.train(labeled_examples) + + def score(self, graph): + """ + Converts the graph into a feature-based representation of + each edge, and then assigns a score to each based on the + confidence of the classifier in assigning it to the + positive label. Scores are returned in a multidimensional list. + + :type graph: DependencyGraph + :param graph: A dependency graph to score. + :rtype: 3 dimensional list + :return: Edge scores for the graph parameter. + """ + # Convert graph to feature representation + edges = [] + for head_node in graph.nodes.values(): + for child_node in graph.nodes.values(): + edges.append( + dict( + a=head_node["word"], + b=head_node["tag"], + c=child_node["word"], + d=child_node["tag"], + ) + ) + + # Score edges + edge_scores = [] + row = [] + count = 0 + for pdist in self.classifier.prob_classify_many(edges): + logger.debug("%.4f %.4f", pdist.prob("T"), pdist.prob("F")) + # smoothing in case the probability = 0 + row.append([math.log(pdist.prob("T") + 0.00000000001)]) + count += 1 + if count == len(graph.nodes): + edge_scores.append(row) + row = [] + count = 0 + return edge_scores + + +################################################################# +# A Scorer for Demo Purposes +################################################################# +# A short class necessary to show parsing example from paper +class DemoScorer(DependencyScorerI): + def train(self, graphs): + print("Training...") + + def score(self, graph): + # scores for Keith Hall 'K-best Spanning Tree Parsing' paper + return [ + [[], [5], [1], [1]], + [[], [], [11], [4]], + [[], [10], [], [5]], + [[], [8], [8], []], + ] + + +################################################################# +# Non-Projective Probabilistic Parsing +################################################################# + + +class ProbabilisticNonprojectiveParser: + """A probabilistic non-projective dependency parser. + + Nonprojective dependencies allows for "crossing branches" in the parse tree + which is necessary for representing particular linguistic phenomena, or even + typical parses in some languages. This parser follows the MST parsing + algorithm, outlined in McDonald(2005), which likens the search for the best + non-projective parse to finding the maximum spanning tree in a weighted + directed graph. + + >>> class Scorer(DependencyScorerI): + ... def train(self, graphs): + ... pass + ... + ... def score(self, graph): + ... return [ + ... [[], [5], [1], [1]], + ... [[], [], [11], [4]], + ... [[], [10], [], [5]], + ... [[], [8], [8], []], + ... ] + + + >>> npp = ProbabilisticNonprojectiveParser() + >>> npp.train([], Scorer()) + + >>> parses = npp.parse(['v1', 'v2', 'v3'], [None, None, None]) + >>> len(list(parses)) + 1 + + Rule based example + + >>> from nltk.grammar import DependencyGrammar + + >>> grammar = DependencyGrammar.fromstring(''' + ... 'taught' -> 'play' | 'man' + ... 'man' -> 'the' | 'in' + ... 'in' -> 'corner' + ... 'corner' -> 'the' + ... 'play' -> 'golf' | 'dachshund' | 'to' + ... 'dachshund' -> 'his' + ... ''') + + >>> ndp = NonprojectiveDependencyParser(grammar) + >>> parses = ndp.parse(['the', 'man', 'in', 'the', 'corner', 'taught', 'his', 'dachshund', 'to', 'play', 'golf']) + >>> len(list(parses)) + 4 + + """ + + def __init__(self): + """ + Creates a new non-projective parser. + """ + logging.debug("initializing prob. nonprojective...") + + def train(self, graphs, dependency_scorer): + """ + Trains a ``DependencyScorerI`` from a set of ``DependencyGraph`` objects, + and establishes this as the parser's scorer. This is used to + initialize the scores on a ``DependencyGraph`` during the parsing + procedure. + + :type graphs: list(DependencyGraph) + :param graphs: A list of dependency graphs to train the scorer. + :type dependency_scorer: DependencyScorerI + :param dependency_scorer: A scorer which implements the + ``DependencyScorerI`` interface. + """ + self._scorer = dependency_scorer + self._scorer.train(graphs) + + def initialize_edge_scores(self, graph): + """ + Assigns a score to every edge in the ``DependencyGraph`` graph. + These scores are generated via the parser's scorer which + was assigned during the training process. + + :type graph: DependencyGraph + :param graph: A dependency graph to assign scores to. + """ + self.scores = self._scorer.score(graph) + + def collapse_nodes(self, new_node, cycle_path, g_graph, b_graph, c_graph): + """ + Takes a list of nodes that have been identified to belong to a cycle, + and collapses them into on larger node. The arcs of all nodes in + the graph must be updated to account for this. + + :type new_node: Node. + :param new_node: A Node (Dictionary) to collapse the cycle nodes into. + :type cycle_path: A list of integers. + :param cycle_path: A list of node addresses, each of which is in the cycle. + :type g_graph, b_graph, c_graph: DependencyGraph + :param g_graph, b_graph, c_graph: Graphs which need to be updated. + """ + logger.debug("Collapsing nodes...") + # Collapse all cycle nodes into v_n+1 in G_Graph + for cycle_node_index in cycle_path: + g_graph.remove_by_address(cycle_node_index) + g_graph.add_node(new_node) + g_graph.redirect_arcs(cycle_path, new_node["address"]) + + def update_edge_scores(self, new_node, cycle_path): + """ + Updates the edge scores to reflect a collapse operation into + new_node. + + :type new_node: A Node. + :param new_node: The node which cycle nodes are collapsed into. + :type cycle_path: A list of integers. + :param cycle_path: A list of node addresses that belong to the cycle. + """ + logger.debug("cycle %s", cycle_path) + + cycle_path = self.compute_original_indexes(cycle_path) + + logger.debug("old cycle %s", cycle_path) + logger.debug("Prior to update: %s", self.scores) + + for i, row in enumerate(self.scores): + for j, column in enumerate(self.scores[i]): + logger.debug(self.scores[i][j]) + if j in cycle_path and i not in cycle_path and self.scores[i][j]: + subtract_val = self.compute_max_subtract_score(j, cycle_path) + + logger.debug("%s - %s", self.scores[i][j], subtract_val) + + new_vals = [] + for cur_val in self.scores[i][j]: + new_vals.append(cur_val - subtract_val) + + self.scores[i][j] = new_vals + + for i, row in enumerate(self.scores): + for j, cell in enumerate(self.scores[i]): + if i in cycle_path and j in cycle_path: + self.scores[i][j] = [] + + logger.debug("After update: %s", self.scores) + + def compute_original_indexes(self, new_indexes): + """ + As nodes are collapsed into others, they are replaced + by the new node in the graph, but it's still necessary + to keep track of what these original nodes were. This + takes a list of node addresses and replaces any collapsed + node addresses with their original addresses. + + :type new_indexes: A list of integers. + :param new_indexes: A list of node addresses to check for + subsumed nodes. + """ + swapped = True + while swapped: + originals = [] + swapped = False + for new_index in new_indexes: + if new_index in self.inner_nodes: + for old_val in self.inner_nodes[new_index]: + if old_val not in originals: + originals.append(old_val) + swapped = True + else: + originals.append(new_index) + new_indexes = originals + return new_indexes + + def compute_max_subtract_score(self, column_index, cycle_indexes): + """ + When updating scores the score of the highest-weighted incoming + arc is subtracted upon collapse. This returns the correct + amount to subtract from that edge. + + :type column_index: integer. + :param column_index: A index representing the column of incoming arcs + to a particular node being updated + :type cycle_indexes: A list of integers. + :param cycle_indexes: Only arcs from cycle nodes are considered. This + is a list of such nodes addresses. + """ + max_score = -100000 + for row_index in cycle_indexes: + for subtract_val in self.scores[row_index][column_index]: + if subtract_val > max_score: + max_score = subtract_val + return max_score + + def best_incoming_arc(self, node_index): + """ + Returns the source of the best incoming arc to the + node with address: node_index + + :type node_index: integer. + :param node_index: The address of the 'destination' node, + the node that is arced to. + """ + originals = self.compute_original_indexes([node_index]) + logger.debug("originals: %s", originals) + + max_arc = None + max_score = None + for row_index in range(len(self.scores)): + for col_index in range(len(self.scores[row_index])): + if col_index in originals and ( + max_score is None or self.scores[row_index][col_index] > max_score + ): + max_score = self.scores[row_index][col_index] + max_arc = row_index + logger.debug("%s, %s", row_index, col_index) + + logger.debug(max_score) + + for key in self.inner_nodes: + replaced_nodes = self.inner_nodes[key] + if max_arc in replaced_nodes: + return key + + return max_arc + + def original_best_arc(self, node_index): + originals = self.compute_original_indexes([node_index]) + max_arc = None + max_score = None + max_orig = None + for row_index in range(len(self.scores)): + for col_index in range(len(self.scores[row_index])): + if col_index in originals and ( + max_score is None or self.scores[row_index][col_index] > max_score + ): + max_score = self.scores[row_index][col_index] + max_arc = row_index + max_orig = col_index + return [max_arc, max_orig] + + def parse(self, tokens, tags): + """ + Parses a list of tokens in accordance to the MST parsing algorithm + for non-projective dependency parses. Assumes that the tokens to + be parsed have already been tagged and those tags are provided. Various + scoring methods can be used by implementing the ``DependencyScorerI`` + interface and passing it to the training algorithm. + + :type tokens: list(str) + :param tokens: A list of words or punctuation to be parsed. + :type tags: list(str) + :param tags: A list of tags corresponding by index to the words in the tokens list. + :return: An iterator of non-projective parses. + :rtype: iter(DependencyGraph) + """ + self.inner_nodes = {} + + # Initialize g_graph + g_graph = DependencyGraph() + for index, token in enumerate(tokens): + g_graph.nodes[index + 1].update( + {"word": token, "tag": tags[index], "rel": "NTOP", "address": index + 1} + ) + + # Fully connect non-root nodes in g_graph + g_graph.connect_graph() + original_graph = DependencyGraph() + for index, token in enumerate(tokens): + original_graph.nodes[index + 1].update( + {"word": token, "tag": tags[index], "rel": "NTOP", "address": index + 1} + ) + + b_graph = DependencyGraph() + c_graph = DependencyGraph() + + for index, token in enumerate(tokens): + c_graph.nodes[index + 1].update( + {"word": token, "tag": tags[index], "rel": "NTOP", "address": index + 1} + ) + + # Assign initial scores to g_graph edges + self.initialize_edge_scores(g_graph) + logger.debug(self.scores) + # Initialize a list of unvisited vertices (by node address) + unvisited_vertices = [vertex["address"] for vertex in c_graph.nodes.values()] + # Iterate over unvisited vertices + nr_vertices = len(tokens) + betas = {} + while unvisited_vertices: + # Mark current node as visited + current_vertex = unvisited_vertices.pop(0) + logger.debug("current_vertex: %s", current_vertex) + # Get corresponding node n_i to vertex v_i + current_node = g_graph.get_by_address(current_vertex) + logger.debug("current_node: %s", current_node) + # Get best in-edge node b for current node + best_in_edge = self.best_incoming_arc(current_vertex) + betas[current_vertex] = self.original_best_arc(current_vertex) + logger.debug("best in arc: %s --> %s", best_in_edge, current_vertex) + # b_graph = Union(b_graph, b) + for new_vertex in [current_vertex, best_in_edge]: + b_graph.nodes[new_vertex].update( + {"word": "TEMP", "rel": "NTOP", "address": new_vertex} + ) + b_graph.add_arc(best_in_edge, current_vertex) + # Beta(current node) = b - stored for parse recovery + # If b_graph contains a cycle, collapse it + cycle_path = b_graph.contains_cycle() + if cycle_path: + # Create a new node v_n+1 with address = len(nodes) + 1 + new_node = {"word": "NONE", "rel": "NTOP", "address": nr_vertices + 1} + # c_graph = Union(c_graph, v_n+1) + c_graph.add_node(new_node) + # Collapse all nodes in cycle C into v_n+1 + self.update_edge_scores(new_node, cycle_path) + self.collapse_nodes(new_node, cycle_path, g_graph, b_graph, c_graph) + for cycle_index in cycle_path: + c_graph.add_arc(new_node["address"], cycle_index) + # self.replaced_by[cycle_index] = new_node['address'] + + self.inner_nodes[new_node["address"]] = cycle_path + + # Add v_n+1 to list of unvisited vertices + unvisited_vertices.insert(0, nr_vertices + 1) + + # increment # of nodes counter + nr_vertices += 1 + + # Remove cycle nodes from b_graph; B = B - cycle c + for cycle_node_address in cycle_path: + b_graph.remove_by_address(cycle_node_address) + + logger.debug("g_graph: %s", g_graph) + logger.debug("b_graph: %s", b_graph) + logger.debug("c_graph: %s", c_graph) + logger.debug("Betas: %s", betas) + logger.debug("replaced nodes %s", self.inner_nodes) + + # Recover parse tree + logger.debug("Final scores: %s", self.scores) + + logger.debug("Recovering parse...") + for i in range(len(tokens) + 1, nr_vertices + 1): + betas[betas[i][1]] = betas[i] + + logger.debug("Betas: %s", betas) + for node in original_graph.nodes.values(): + # TODO: It's dangerous to assume that deps it a dictionary + # because it's a default dictionary. Ideally, here we should not + # be concerned how dependencies are stored inside of a dependency + # graph. + node["deps"] = {} + for i in range(1, len(tokens) + 1): + original_graph.add_arc(betas[i][0], betas[i][1]) + + logger.debug("Done.") + yield original_graph + + +################################################################# +# Rule-based Non-Projective Parser +################################################################# + + +class NonprojectiveDependencyParser: + """ + A non-projective, rule-based, dependency parser. This parser + will return the set of all possible non-projective parses based on + the word-to-word relations defined in the parser's dependency + grammar, and will allow the branches of the parse tree to cross + in order to capture a variety of linguistic phenomena that a + projective parser will not. + """ + + def __init__(self, dependency_grammar): + """ + Creates a new ``NonprojectiveDependencyParser``. + + :param dependency_grammar: a grammar of word-to-word relations. + :type dependency_grammar: DependencyGrammar + """ + self._grammar = dependency_grammar + + def parse(self, tokens): + """ + Parses the input tokens with respect to the parser's grammar. Parsing + is accomplished by representing the search-space of possible parses as + a fully-connected directed graph. Arcs that would lead to ungrammatical + parses are removed and a lattice is constructed of length n, where n is + the number of input tokens, to represent all possible grammatical + traversals. All possible paths through the lattice are then enumerated + to produce the set of non-projective parses. + + param tokens: A list of tokens to parse. + type tokens: list(str) + return: An iterator of non-projective parses. + rtype: iter(DependencyGraph) + """ + # Create graph representation of tokens + self._graph = DependencyGraph() + + for index, token in enumerate(tokens): + self._graph.nodes[index] = { + "word": token, + "deps": [], + "rel": "NTOP", + "address": index, + } + + for head_node in self._graph.nodes.values(): + deps = [] + for dep_node in self._graph.nodes.values(): + if ( + self._grammar.contains(head_node["word"], dep_node["word"]) + and head_node["word"] != dep_node["word"] + ): + deps.append(dep_node["address"]) + head_node["deps"] = deps + + # Create lattice of possible heads + roots = [] + possible_heads = [] + for i, word in enumerate(tokens): + heads = [] + for j, head in enumerate(tokens): + if (i != j) and self._grammar.contains(head, word): + heads.append(j) + if len(heads) == 0: + roots.append(i) + possible_heads.append(heads) + + # Set roots to attempt + if len(roots) < 2: + if len(roots) == 0: + for i in range(len(tokens)): + roots.append(i) + + # Traverse lattice + analyses = [] + for _ in roots: + stack = [] + analysis = [[] for i in range(len(possible_heads))] + i = 0 + forward = True + while i >= 0: + if forward: + if len(possible_heads[i]) == 1: + analysis[i] = possible_heads[i][0] + elif len(possible_heads[i]) == 0: + analysis[i] = -1 + else: + head = possible_heads[i].pop() + analysis[i] = head + stack.append([i, head]) + if not forward: + index_on_stack = False + for stack_item in stack: + if stack_item[0] == i: + index_on_stack = True + orig_length = len(possible_heads[i]) + + if index_on_stack and orig_length == 0: + for j in range(len(stack) - 1, -1, -1): + stack_item = stack[j] + if stack_item[0] == i: + possible_heads[i].append(stack.pop(j)[1]) + + elif index_on_stack and orig_length > 0: + head = possible_heads[i].pop() + analysis[i] = head + stack.append([i, head]) + forward = True + + if i + 1 == len(possible_heads): + analyses.append(analysis[:]) + forward = False + if forward: + i += 1 + else: + i -= 1 + + # Filter parses + # ensure 1 root, every thing has 1 head + for analysis in analyses: + if analysis.count(-1) > 1: + # there are several root elements! + continue + + graph = DependencyGraph() + graph.root = graph.nodes[analysis.index(-1) + 1] + + for address, (token, head_index) in enumerate( + zip(tokens, analysis), start=1 + ): + head_address = head_index + 1 + + node = graph.nodes[address] + node.update({"word": token, "address": address}) + + if head_address == 0: + rel = "ROOT" + else: + rel = "" + graph.nodes[head_index + 1]["deps"][rel].append(address) + + # TODO: check for cycles + yield graph + + +################################################################# +# Demos +################################################################# + + +def demo(): + # hall_demo() + nonprojective_conll_parse_demo() + rule_based_demo() + + +def hall_demo(): + npp = ProbabilisticNonprojectiveParser() + npp.train([], DemoScorer()) + for parse_graph in npp.parse(["v1", "v2", "v3"], [None, None, None]): + print(parse_graph) + + +def nonprojective_conll_parse_demo(): + from nltk.parse.dependencygraph import conll_data2 + + graphs = [DependencyGraph(entry) for entry in conll_data2.split("\n\n") if entry] + npp = ProbabilisticNonprojectiveParser() + npp.train(graphs, NaiveBayesDependencyScorer()) + for parse_graph in npp.parse( + ["Cathy", "zag", "hen", "zwaaien", "."], ["N", "V", "Pron", "Adj", "N", "Punc"] + ): + print(parse_graph) + + +def rule_based_demo(): + from nltk.grammar import DependencyGrammar + + grammar = DependencyGrammar.fromstring( + """ + 'taught' -> 'play' | 'man' + 'man' -> 'the' | 'in' + 'in' -> 'corner' + 'corner' -> 'the' + 'play' -> 'golf' | 'dachshund' | 'to' + 'dachshund' -> 'his' + """ + ) + print(grammar) + ndp = NonprojectiveDependencyParser(grammar) + graphs = ndp.parse( + [ + "the", + "man", + "in", + "the", + "corner", + "taught", + "his", + "dachshund", + "to", + "play", + "golf", + ] + ) + print("Graphs:") + for graph in graphs: + print(graph) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/pchart.py b/nltk/parse/pchart.py new file mode 100644 index 0000000..990f4d0 --- /dev/null +++ b/nltk/parse/pchart.py @@ -0,0 +1,579 @@ +# Natural Language Toolkit: Probabilistic Chart Parsers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Classes and interfaces for associating probabilities with tree +structures that represent the internal organization of a text. The +probabilistic parser module defines ``BottomUpProbabilisticChartParser``. + +``BottomUpProbabilisticChartParser`` is an abstract class that implements +a bottom-up chart parser for ``PCFG`` grammars. It maintains a queue of edges, +and adds them to the chart one at a time. The ordering of this queue +is based on the probabilities associated with the edges, allowing the +parser to expand more likely edges before less likely ones. Each +subclass implements a different queue ordering, producing different +search strategies. Currently the following subclasses are defined: + + - ``InsideChartParser`` searches edges in decreasing order of + their trees' inside probabilities. + - ``RandomChartParser`` searches edges in random order. + - ``LongestChartParser`` searches edges in decreasing order of their + location's length. + +The ``BottomUpProbabilisticChartParser`` constructor has an optional +argument beam_size. If non-zero, this controls the size of the beam +(aka the edge queue). This option is most useful with InsideChartParser. +""" + +##////////////////////////////////////////////////////// +## Bottom-Up PCFG Chart Parser +##////////////////////////////////////////////////////// + +# [XX] This might not be implemented quite right -- it would be better +# to associate probabilities with child pointer lists. + +import random +from functools import reduce + +from nltk.grammar import PCFG, Nonterminal +from nltk.parse.api import ParserI +from nltk.parse.chart import AbstractChartRule, Chart, LeafEdge, TreeEdge +from nltk.tree import ProbabilisticTree, Tree + + +# Probabilistic edges +class ProbabilisticLeafEdge(LeafEdge): + def prob(self): + return 1.0 + + +class ProbabilisticTreeEdge(TreeEdge): + def __init__(self, prob, *args, **kwargs): + TreeEdge.__init__(self, *args, **kwargs) + self._prob = prob + # two edges with different probabilities are not equal. + self._comparison_key = (self._comparison_key, prob) + + def prob(self): + return self._prob + + @staticmethod + def from_production(production, index, p): + return ProbabilisticTreeEdge( + p, (index, index), production.lhs(), production.rhs(), 0 + ) + + +# Rules using probabilistic edges +class ProbabilisticBottomUpInitRule(AbstractChartRule): + NUM_EDGES = 0 + + def apply(self, chart, grammar): + for index in range(chart.num_leaves()): + new_edge = ProbabilisticLeafEdge(chart.leaf(index), index) + if chart.insert(new_edge, ()): + yield new_edge + + +class ProbabilisticBottomUpPredictRule(AbstractChartRule): + NUM_EDGES = 1 + + def apply(self, chart, grammar, edge): + if edge.is_incomplete(): + return + for prod in grammar.productions(): + if edge.lhs() == prod.rhs()[0]: + new_edge = ProbabilisticTreeEdge.from_production( + prod, edge.start(), prod.prob() + ) + if chart.insert(new_edge, ()): + yield new_edge + + +class ProbabilisticFundamentalRule(AbstractChartRule): + NUM_EDGES = 2 + + def apply(self, chart, grammar, left_edge, right_edge): + # Make sure the rule is applicable. + if not ( + left_edge.end() == right_edge.start() + and left_edge.nextsym() == right_edge.lhs() + and left_edge.is_incomplete() + and right_edge.is_complete() + ): + return + + # Construct the new edge. + p = left_edge.prob() * right_edge.prob() + new_edge = ProbabilisticTreeEdge( + p, + span=(left_edge.start(), right_edge.end()), + lhs=left_edge.lhs(), + rhs=left_edge.rhs(), + dot=left_edge.dot() + 1, + ) + + # Add it to the chart, with appropriate child pointers. + changed_chart = False + for cpl1 in chart.child_pointer_lists(left_edge): + if chart.insert(new_edge, cpl1 + (right_edge,)): + changed_chart = True + + # If we changed the chart, then generate the edge. + if changed_chart: + yield new_edge + + +class SingleEdgeProbabilisticFundamentalRule(AbstractChartRule): + NUM_EDGES = 1 + + _fundamental_rule = ProbabilisticFundamentalRule() + + def apply(self, chart, grammar, edge1): + fr = self._fundamental_rule + if edge1.is_incomplete(): + # edge1 = left_edge; edge2 = right_edge + for edge2 in chart.select( + start=edge1.end(), is_complete=True, lhs=edge1.nextsym() + ): + yield from fr.apply(chart, grammar, edge1, edge2) + else: + # edge2 = left_edge; edge1 = right_edge + for edge2 in chart.select( + end=edge1.start(), is_complete=False, nextsym=edge1.lhs() + ): + yield from fr.apply(chart, grammar, edge2, edge1) + + def __str__(self): + return "Fundamental Rule" + + +class BottomUpProbabilisticChartParser(ParserI): + """ + An abstract bottom-up parser for ``PCFG`` grammars that uses a ``Chart`` to + record partial results. ``BottomUpProbabilisticChartParser`` maintains + a queue of edges that can be added to the chart. This queue is + initialized with edges for each token in the text that is being + parsed. ``BottomUpProbabilisticChartParser`` inserts these edges into + the chart one at a time, starting with the most likely edges, and + proceeding to less likely edges. For each edge that is added to + the chart, it may become possible to insert additional edges into + the chart; these are added to the queue. This process continues + until enough complete parses have been generated, or until the + queue is empty. + + The sorting order for the queue is not specified by + ``BottomUpProbabilisticChartParser``. Different sorting orders will + result in different search strategies. The sorting order for the + queue is defined by the method ``sort_queue``; subclasses are required + to provide a definition for this method. + + :type _grammar: PCFG + :ivar _grammar: The grammar used to parse sentences. + :type _trace: int + :ivar _trace: The level of tracing output that should be generated + when parsing a text. + """ + + def __init__(self, grammar, beam_size=0, trace=0): + """ + Create a new ``BottomUpProbabilisticChartParser``, that uses + ``grammar`` to parse texts. + + :type grammar: PCFG + :param grammar: The grammar used to parse texts. + :type beam_size: int + :param beam_size: The maximum length for the parser's edge queue. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing + output. + """ + if not isinstance(grammar, PCFG): + raise ValueError("The grammar must be probabilistic PCFG") + self._grammar = grammar + self.beam_size = beam_size + self._trace = trace + + def grammar(self): + return self._grammar + + def trace(self, trace=2): + """ + Set the level of tracing output that should be generated when + parsing a text. + + :type trace: int + :param trace: The trace level. A trace level of ``0`` will + generate no tracing output; and higher trace levels will + produce more verbose tracing output. + :rtype: None + """ + self._trace = trace + + # TODO: change this to conform more with the standard ChartParser + def parse(self, tokens): + self._grammar.check_coverage(tokens) + chart = Chart(list(tokens)) + grammar = self._grammar + + # Chart parser rules. + bu_init = ProbabilisticBottomUpInitRule() + bu = ProbabilisticBottomUpPredictRule() + fr = SingleEdgeProbabilisticFundamentalRule() + + # Our queue + queue = [] + + # Initialize the chart. + for edge in bu_init.apply(chart, grammar): + if self._trace > 1: + print( + " %-50s [%s]" + % (chart.pretty_format_edge(edge, width=2), edge.prob()) + ) + queue.append(edge) + + while len(queue) > 0: + # Re-sort the queue. + self.sort_queue(queue, chart) + + # Prune the queue to the correct size if a beam was defined + if self.beam_size: + self._prune(queue, chart) + + # Get the best edge. + edge = queue.pop() + if self._trace > 0: + print( + " %-50s [%s]" + % (chart.pretty_format_edge(edge, width=2), edge.prob()) + ) + + # Apply BU & FR to it. + queue.extend(bu.apply(chart, grammar, edge)) + queue.extend(fr.apply(chart, grammar, edge)) + + # Get a list of complete parses. + parses = list(chart.parses(grammar.start(), ProbabilisticTree)) + + # Assign probabilities to the trees. + prod_probs = {} + for prod in grammar.productions(): + prod_probs[prod.lhs(), prod.rhs()] = prod.prob() + for parse in parses: + self._setprob(parse, prod_probs) + + # Sort by probability + parses.sort(reverse=True, key=lambda tree: tree.prob()) + + return iter(parses) + + def _setprob(self, tree, prod_probs): + if tree.prob() is not None: + return + + # Get the prob of the CFG production. + lhs = Nonterminal(tree.label()) + rhs = [] + for child in tree: + if isinstance(child, Tree): + rhs.append(Nonterminal(child.label())) + else: + rhs.append(child) + prob = prod_probs[lhs, tuple(rhs)] + + # Get the probs of children. + for child in tree: + if isinstance(child, Tree): + self._setprob(child, prod_probs) + prob *= child.prob() + + tree.set_prob(prob) + + def sort_queue(self, queue, chart): + """ + Sort the given queue of ``Edge`` objects, placing the edge that should + be tried first at the beginning of the queue. This method + will be called after each ``Edge`` is added to the queue. + + :param queue: The queue of ``Edge`` objects to sort. Each edge in + this queue is an edge that could be added to the chart by + the fundamental rule; but that has not yet been added. + :type queue: list(Edge) + :param chart: The chart being used to parse the text. This + chart can be used to provide extra information for sorting + the queue. + :type chart: Chart + :rtype: None + """ + raise NotImplementedError() + + def _prune(self, queue, chart): + """Discard items in the queue if the queue is longer than the beam.""" + if len(queue) > self.beam_size: + split = len(queue) - self.beam_size + if self._trace > 2: + for edge in queue[:split]: + print(" %-50s [DISCARDED]" % chart.pretty_format_edge(edge, 2)) + del queue[:split] + + +class InsideChartParser(BottomUpProbabilisticChartParser): + """ + A bottom-up parser for ``PCFG`` grammars that tries edges in descending + order of the inside probabilities of their trees. The "inside + probability" of a tree is simply the + probability of the entire tree, ignoring its context. In + particular, the inside probability of a tree generated by + production *p* with children *c[1], c[2], ..., c[n]* is + *P(p)P(c[1])P(c[2])...P(c[n])*; and the inside + probability of a token is 1 if it is present in the text, and 0 if + it is absent. + + This sorting order results in a type of lowest-cost-first search + strategy. + """ + + # Inherit constructor. + def sort_queue(self, queue, chart): + """ + Sort the given queue of edges, in descending order of the + inside probabilities of the edges' trees. + + :param queue: The queue of ``Edge`` objects to sort. Each edge in + this queue is an edge that could be added to the chart by + the fundamental rule; but that has not yet been added. + :type queue: list(Edge) + :param chart: The chart being used to parse the text. This + chart can be used to provide extra information for sorting + the queue. + :type chart: Chart + :rtype: None + """ + queue.sort(key=lambda edge: edge.prob()) + + +# Eventually, this will become some sort of inside-outside parser: +# class InsideOutsideParser(BottomUpProbabilisticChartParser): +# def __init__(self, grammar, trace=0): +# # Inherit docs. +# BottomUpProbabilisticChartParser.__init__(self, grammar, trace) +# +# # Find the best path from S to each nonterminal +# bestp = {} +# for production in grammar.productions(): bestp[production.lhs()]=0 +# bestp[grammar.start()] = 1.0 +# +# for i in range(len(grammar.productions())): +# for production in grammar.productions(): +# lhs = production.lhs() +# for elt in production.rhs(): +# bestp[elt] = max(bestp[lhs]*production.prob(), +# bestp.get(elt,0)) +# +# self._bestp = bestp +# for (k,v) in self._bestp.items(): print(k,v) +# +# def _sortkey(self, edge): +# return edge.structure()[PROB] * self._bestp[edge.lhs()] +# +# def sort_queue(self, queue, chart): +# queue.sort(key=self._sortkey) + + +class RandomChartParser(BottomUpProbabilisticChartParser): + """ + A bottom-up parser for ``PCFG`` grammars that tries edges in random order. + This sorting order results in a random search strategy. + """ + + # Inherit constructor + def sort_queue(self, queue, chart): + i = random.randint(0, len(queue) - 1) + (queue[-1], queue[i]) = (queue[i], queue[-1]) + + +class UnsortedChartParser(BottomUpProbabilisticChartParser): + """ + A bottom-up parser for ``PCFG`` grammars that tries edges in whatever order. + """ + + # Inherit constructor + def sort_queue(self, queue, chart): + return + + +class LongestChartParser(BottomUpProbabilisticChartParser): + """ + A bottom-up parser for ``PCFG`` grammars that tries longer edges before + shorter ones. This sorting order results in a type of best-first + search strategy. + """ + + # Inherit constructor + def sort_queue(self, queue, chart): + queue.sort(key=lambda edge: edge.length()) + + +##////////////////////////////////////////////////////// +## Test Code +##////////////////////////////////////////////////////// + + +def demo(choice=None, draw_parses=None, print_parses=None): + """ + A demonstration of the probabilistic parsers. The user is + prompted to select which demo to run, and how many parses should + be found; and then each parser is run on the same demo, and a + summary of the results are displayed. + """ + import sys + import time + + from nltk import tokenize + from nltk.parse import pchart + + # Define two demos. Each demo has a sentence and a grammar. + toy_pcfg1 = PCFG.fromstring( + """ + S -> NP VP [1.0] + NP -> Det N [0.5] | NP PP [0.25] | 'John' [0.1] | 'I' [0.15] + Det -> 'the' [0.8] | 'my' [0.2] + N -> 'man' [0.5] | 'telescope' [0.5] + VP -> VP PP [0.1] | V NP [0.7] | V [0.2] + V -> 'ate' [0.35] | 'saw' [0.65] + PP -> P NP [1.0] + P -> 'with' [0.61] | 'under' [0.39] + """ + ) + + toy_pcfg2 = PCFG.fromstring( + """ + S -> NP VP [1.0] + VP -> V NP [.59] + VP -> V [.40] + VP -> VP PP [.01] + NP -> Det N [.41] + NP -> Name [.28] + NP -> NP PP [.31] + PP -> P NP [1.0] + V -> 'saw' [.21] + V -> 'ate' [.51] + V -> 'ran' [.28] + N -> 'boy' [.11] + N -> 'cookie' [.12] + N -> 'table' [.13] + N -> 'telescope' [.14] + N -> 'hill' [.5] + Name -> 'Jack' [.52] + Name -> 'Bob' [.48] + P -> 'with' [.61] + P -> 'under' [.39] + Det -> 'the' [.41] + Det -> 'a' [.31] + Det -> 'my' [.28] + """ + ) + + demos = [ + ("I saw John with my telescope", toy_pcfg1), + ("the boy saw Jack with Bob under the table with a telescope", toy_pcfg2), + ] + + if choice is None: + # Ask the user which demo they want to use. + print() + for i in range(len(demos)): + print(f"{i + 1:>3}: {demos[i][0]}") + print(" %r" % demos[i][1]) + print() + print("Which demo (%d-%d)? " % (1, len(demos)), end=" ") + choice = int(sys.stdin.readline().strip()) - 1 + try: + sent, grammar = demos[choice] + except Exception: + print("Bad sentence number") + return + + # Tokenize the sentence. + tokens = sent.split() + + # Define a list of parsers. We'll use all parsers. + parsers = [ + pchart.InsideChartParser(grammar), + pchart.RandomChartParser(grammar), + pchart.UnsortedChartParser(grammar), + pchart.LongestChartParser(grammar), + pchart.InsideChartParser(grammar, beam_size=len(tokens) + 1), # was BeamParser + ] + + # Run the parsers on the tokenized sentence. + times = [] + average_p = [] + num_parses = [] + all_parses = {} + for parser in parsers: + print(f"\ns: {sent}\nparser: {parser}\ngrammar: {grammar}") + parser.trace(3) + t = time.time() + parses = list(parser.parse(tokens)) + times.append(time.time() - t) + p = reduce(lambda a, b: a + b.prob(), parses, 0) / len(parses) if parses else 0 + average_p.append(p) + num_parses.append(len(parses)) + for p in parses: + all_parses[p.freeze()] = 1 + + # Print some summary statistics + print() + print(" Parser Beam | Time (secs) # Parses Average P(parse)") + print("------------------------+------------------------------------------") + for i in range(len(parsers)): + print( + "%18s %4d |%11.4f%11d%19.14f" + % ( + parsers[i].__class__.__name__, + parsers[i].beam_size, + times[i], + num_parses[i], + average_p[i], + ) + ) + parses = all_parses.keys() + if parses: + p = reduce(lambda a, b: a + b.prob(), parses, 0) / len(parses) + else: + p = 0 + print("------------------------+------------------------------------------") + print("%18s |%11s%11d%19.14f" % ("(All Parses)", "n/a", len(parses), p)) + + if draw_parses is None: + # Ask the user if we should draw the parses. + print() + print("Draw parses (y/n)? ", end=" ") + draw_parses = sys.stdin.readline().strip().lower().startswith("y") + if draw_parses: + from nltk.draw.tree import draw_trees + + print(" please wait...") + draw_trees(*parses) + + if print_parses is None: + # Ask the user if we should print the parses. + print() + print("Print parses (y/n)? ", end=" ") + print_parses = sys.stdin.readline().strip().lower().startswith("y") + if print_parses: + for parse in parses: + print(parse) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/projectivedependencyparser.py b/nltk/parse/projectivedependencyparser.py new file mode 100644 index 0000000..e5f79c3 --- /dev/null +++ b/nltk/parse/projectivedependencyparser.py @@ -0,0 +1,745 @@ +# Natural Language Toolkit: Dependency Grammars +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Jason Narad +# +# URL: +# For license information, see LICENSE.TXT +# + +from collections import defaultdict +from functools import total_ordering +from itertools import chain + +from nltk.grammar import ( + DependencyGrammar, + DependencyProduction, + ProbabilisticDependencyGrammar, +) +from nltk.internals import raise_unorderable_types +from nltk.parse.dependencygraph import DependencyGraph + +################################################################# +# Dependency Span +################################################################# + + +@total_ordering +class DependencySpan: + """ + A contiguous span over some part of the input string representing + dependency (head -> modifier) relationships amongst words. An atomic + span corresponds to only one word so it isn't a 'span' in the conventional + sense, as its _start_index = _end_index = _head_index for concatenation + purposes. All other spans are assumed to have arcs between all nodes + within the start and end indexes of the span, and one head index corresponding + to the head word for the entire span. This is the same as the root node if + the dependency structure were depicted as a graph. + """ + + def __init__(self, start_index, end_index, head_index, arcs, tags): + self._start_index = start_index + self._end_index = end_index + self._head_index = head_index + self._arcs = arcs + self._tags = tags + self._comparison_key = (start_index, end_index, head_index, tuple(arcs)) + self._hash = hash(self._comparison_key) + + def head_index(self): + """ + :return: An value indexing the head of the entire ``DependencySpan``. + :rtype: int + """ + return self._head_index + + def __repr__(self): + """ + :return: A concise string representatino of the ``DependencySpan``. + :rtype: str. + """ + return "Span %d-%d; Head Index: %d" % ( + self._start_index, + self._end_index, + self._head_index, + ) + + def __str__(self): + """ + :return: A verbose string representation of the ``DependencySpan``. + :rtype: str + """ + str = "Span %d-%d; Head Index: %d" % ( + self._start_index, + self._end_index, + self._head_index, + ) + for i in range(len(self._arcs)): + str += "\n%d <- %d, %s" % (i, self._arcs[i], self._tags[i]) + return str + + def __eq__(self, other): + return ( + type(self) == type(other) and self._comparison_key == other._comparison_key + ) + + def __ne__(self, other): + return not self == other + + def __lt__(self, other): + if not isinstance(other, DependencySpan): + raise_unorderable_types("<", self, other) + return self._comparison_key < other._comparison_key + + def __hash__(self): + """ + :return: The hash value of this ``DependencySpan``. + """ + return self._hash + + +################################################################# +# Chart Cell +################################################################# + + +class ChartCell: + """ + A cell from the parse chart formed when performing the CYK algorithm. + Each cell keeps track of its x and y coordinates (though this will probably + be discarded), and a list of spans serving as the cell's entries. + """ + + def __init__(self, x, y): + """ + :param x: This cell's x coordinate. + :type x: int. + :param y: This cell's y coordinate. + :type y: int. + """ + self._x = x + self._y = y + self._entries = set() + + def add(self, span): + """ + Appends the given span to the list of spans + representing the chart cell's entries. + + :param span: The span to add. + :type span: DependencySpan + """ + self._entries.add(span) + + def __str__(self): + """ + :return: A verbose string representation of this ``ChartCell``. + :rtype: str. + """ + return "CC[%d,%d]: %s" % (self._x, self._y, self._entries) + + def __repr__(self): + """ + :return: A concise string representation of this ``ChartCell``. + :rtype: str. + """ + return "%s" % self + + +################################################################# +# Parsing with Dependency Grammars +################################################################# + + +class ProjectiveDependencyParser: + """ + A projective, rule-based, dependency parser. A ProjectiveDependencyParser + is created with a DependencyGrammar, a set of productions specifying + word-to-word dependency relations. The parse() method will then + return the set of all parses, in tree representation, for a given input + sequence of tokens. Each parse must meet the requirements of the both + the grammar and the projectivity constraint which specifies that the + branches of the dependency tree are not allowed to cross. Alternatively, + this can be understood as stating that each parent node and its children + in the parse tree form a continuous substring of the input sequence. + """ + + #: Maximum number of input tokens :meth:`parse` will accept. The Eisner-style + #: span-concatenation algorithm eagerly builds a dense ``(n+1)x(n+1)`` chart + #: (O(n^2) memory) and runs an unconditional triple loop over it (O(n^3) + #: time) regardless of the grammar, so an over-long token list pins a CPU core + #: and exhausts memory (CWE-407). ``parse`` raises ``ValueError`` beyond this + #: many tokens; raise it if you genuinely need to parse a longer sequence. + MAX_TOKENS = 500 + + def __init__(self, dependency_grammar): + """ + Create a new ProjectiveDependencyParser, from a word-to-word + dependency grammar ``DependencyGrammar``. + + :param dependency_grammar: A word-to-word relation dependencygrammar. + :type dependency_grammar: DependencyGrammar + """ + self._grammar = dependency_grammar + + def parse(self, tokens): + """ + Performs a projective dependency parse on the list of tokens using + a chart-based, span-concatenation algorithm similar to Eisner (1996). + + :param tokens: The list of input tokens. + :type tokens: list(str) + :return: An iterator over parse trees. + :rtype: iter(Tree) + """ + self._tokens = tokens = list(tokens) + if len(self._tokens) > self.MAX_TOKENS: + raise ValueError( + f"Cannot parse {len(self._tokens)} tokens: exceeds MAX_TOKENS " + f"({self.MAX_TOKENS}). Projective dependency parsing builds an " + f"O(n^2) chart and runs an O(n^3) loop regardless of the grammar " + f"(CWE-407); raise MAX_TOKENS to allow a longer sequence." + ) + chart = [] + for i in range(0, len(self._tokens) + 1): + chart.append([]) + for j in range(0, len(self._tokens) + 1): + chart[i].append(ChartCell(i, j)) + if i == j + 1: + chart[i][j].add(DependencySpan(i - 1, i, i - 1, [-1], ["null"])) + + for i in range(1, len(self._tokens) + 1): + for j in range(i - 2, -1, -1): + for k in range(i - 1, j, -1): + for span1 in chart[k][j]._entries: + for span2 in chart[i][k]._entries: + for newspan in self.concatenate(span1, span2): + chart[i][j].add(newspan) + + for parse in chart[len(self._tokens)][0]._entries: + conll_format = "" + # malt_format = "" + for i in range(len(tokens)): + # malt_format += '%s\t%s\t%d\t%s\n' % (tokens[i], 'null', parse._arcs[i] + 1, 'null') + # conll_format += '\t%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n' % (i+1, tokens[i], tokens[i], 'null', 'null', 'null', parse._arcs[i] + 1, 'null', '-', '-') + # Modify to comply with the new Dependency Graph requirement (at least must have an root elements) + conll_format += "\t%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n" % ( + i + 1, + tokens[i], + tokens[i], + "null", + "null", + "null", + parse._arcs[i] + 1, + "ROOT", + "-", + "-", + ) + dg = DependencyGraph(conll_format) + # if self.meets_arity(dg): + yield dg.tree() + + def concatenate(self, span1, span2): + """ + Concatenates the two spans in whichever way possible. This + includes rightward concatenation (from the leftmost word of the + leftmost span to the rightmost word of the rightmost span) and + leftward concatenation (vice-versa) between adjacent spans. Unlike + Eisner's presentation of span concatenation, these spans do not + share or pivot on a particular word/word-index. + + :return: A list of new spans formed through concatenation. + :rtype: list(DependencySpan) + """ + spans = [] + if span1._start_index == span2._start_index: + print("Error: Mismatched spans - replace this with thrown error") + if span1._start_index > span2._start_index: + temp_span = span1 + span1 = span2 + span2 = temp_span + # adjacent rightward covered concatenation + new_arcs = span1._arcs + span2._arcs + new_tags = span1._tags + span2._tags + if self._grammar.contains( + self._tokens[span1._head_index], self._tokens[span2._head_index] + ): + # print('Performing rightward cover %d to %d' % (span1._head_index, span2._head_index)) + new_arcs[span2._head_index - span1._start_index] = span1._head_index + spans.append( + DependencySpan( + span1._start_index, + span2._end_index, + span1._head_index, + new_arcs, + new_tags, + ) + ) + # adjacent leftward covered concatenation + new_arcs = span1._arcs + span2._arcs + if self._grammar.contains( + self._tokens[span2._head_index], self._tokens[span1._head_index] + ): + # print('performing leftward cover %d to %d' % (span2._head_index, span1._head_index)) + new_arcs[span1._head_index - span1._start_index] = span2._head_index + spans.append( + DependencySpan( + span1._start_index, + span2._end_index, + span2._head_index, + new_arcs, + new_tags, + ) + ) + return spans + + +################################################################# +# Parsing with Probabilistic Dependency Grammars +################################################################# + + +class ProbabilisticProjectiveDependencyParser: + """A probabilistic, projective dependency parser. + + This parser returns the most probable projective parse derived from the + probabilistic dependency grammar derived from the train() method. The + probabilistic model is an implementation of Eisner's (1996) Model C, which + conditions on head-word, head-tag, child-word, and child-tag. The decoding + uses a bottom-up chart-based span concatenation algorithm that's identical + to the one utilized by the rule-based projective parser. + + Usage example + + >>> from nltk.parse.dependencygraph import conll_data2 + + >>> graphs = [ + ... DependencyGraph(entry) for entry in conll_data2.split('\\n\\n') if entry + ... ] + + >>> ppdp = ProbabilisticProjectiveDependencyParser() + >>> ppdp.train(graphs) + + >>> sent = ['Cathy', 'zag', 'hen', 'wild', 'zwaaien', '.'] + >>> list(ppdp.parse(sent)) + [Tree('zag', ['Cathy', 'hen', Tree('zwaaien', ['wild', '.'])])] + + """ + + #: Maximum number of input tokens :meth:`parse` will accept. Decoding builds + #: a dense ``(n+1)x(n+1)`` chart (O(n^2) memory) and runs an O(n^3) span loop + #: regardless of the grammar, so an over-long token list pins a CPU core and + #: exhausts memory (CWE-407). ``parse`` raises ``ValueError`` beyond this many + #: tokens; raise it if you genuinely need to parse a longer sequence. + MAX_TOKENS = 500 + + def __init__(self): + """ + Create a new probabilistic dependency parser. No additional + operations are necessary. + """ + + def parse(self, tokens): + """ + Parses the list of tokens subject to the projectivity constraint + and the productions in the parser's grammar. This uses a method + similar to the span-concatenation algorithm defined in Eisner (1996). + It returns the most probable parse derived from the parser's + probabilistic dependency grammar. + """ + self._tokens = tokens = list(tokens) + if len(self._tokens) > self.MAX_TOKENS: + raise ValueError( + f"Cannot parse {len(self._tokens)} tokens: exceeds MAX_TOKENS " + f"({self.MAX_TOKENS}). Projective dependency decoding builds an " + f"O(n^2) chart and runs an O(n^3) loop regardless of the grammar " + f"(CWE-407); raise MAX_TOKENS to allow a longer sequence." + ) + chart = [] + for i in range(0, len(self._tokens) + 1): + chart.append([]) + for j in range(0, len(self._tokens) + 1): + chart[i].append(ChartCell(i, j)) + if i == j + 1: + if tokens[i - 1] in self._grammar._tags: + for tag in self._grammar._tags[tokens[i - 1]]: + chart[i][j].add( + DependencySpan(i - 1, i, i - 1, [-1], [tag]) + ) + else: + print( + "No tag found for input token '%s', parse is impossible." + % tokens[i - 1] + ) + return [] + for i in range(1, len(self._tokens) + 1): + for j in range(i - 2, -1, -1): + for k in range(i - 1, j, -1): + for span1 in chart[k][j]._entries: + for span2 in chart[i][k]._entries: + for newspan in self.concatenate(span1, span2): + chart[i][j].add(newspan) + trees = [] + max_parse = None + max_score = 0 + for parse in chart[len(self._tokens)][0]._entries: + conll_format = "" + malt_format = "" + for i in range(len(tokens)): + malt_format += "%s\t%s\t%d\t%s\n" % ( + tokens[i], + "null", + parse._arcs[i] + 1, + "null", + ) + # conll_format += '\t%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n' % (i+1, tokens[i], tokens[i], parse._tags[i], parse._tags[i], 'null', parse._arcs[i] + 1, 'null', '-', '-') + # Modify to comply with recent change in dependency graph such that there must be a ROOT element. + conll_format += "\t%d\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%s\t%s\n" % ( + i + 1, + tokens[i], + tokens[i], + parse._tags[i], + parse._tags[i], + "null", + parse._arcs[i] + 1, + "ROOT", + "-", + "-", + ) + dg = DependencyGraph(conll_format) + score = self.compute_prob(dg) + trees.append((score, dg.tree())) + trees.sort() + return (tree for (score, tree) in trees) + + def concatenate(self, span1, span2): + """ + Concatenates the two spans in whichever way possible. This + includes rightward concatenation (from the leftmost word of the + leftmost span to the rightmost word of the rightmost span) and + leftward concatenation (vice-versa) between adjacent spans. Unlike + Eisner's presentation of span concatenation, these spans do not + share or pivot on a particular word/word-index. + + :return: A list of new spans formed through concatenation. + :rtype: list(DependencySpan) + """ + spans = [] + if span1._start_index == span2._start_index: + print("Error: Mismatched spans - replace this with thrown error") + if span1._start_index > span2._start_index: + temp_span = span1 + span1 = span2 + span2 = temp_span + # adjacent rightward covered concatenation + new_arcs = span1._arcs + span2._arcs + new_tags = span1._tags + span2._tags + if self._grammar.contains( + self._tokens[span1._head_index], self._tokens[span2._head_index] + ): + new_arcs[span2._head_index - span1._start_index] = span1._head_index + spans.append( + DependencySpan( + span1._start_index, + span2._end_index, + span1._head_index, + new_arcs, + new_tags, + ) + ) + # adjacent leftward covered concatenation + new_arcs = span1._arcs + span2._arcs + new_tags = span1._tags + span2._tags + if self._grammar.contains( + self._tokens[span2._head_index], self._tokens[span1._head_index] + ): + new_arcs[span1._head_index - span1._start_index] = span2._head_index + spans.append( + DependencySpan( + span1._start_index, + span2._end_index, + span2._head_index, + new_arcs, + new_tags, + ) + ) + return spans + + def train(self, graphs): + """ + Trains a ProbabilisticDependencyGrammar based on the list of input + DependencyGraphs. This model is an implementation of Eisner's (1996) + Model C, which derives its statistics from head-word, head-tag, + child-word, and child-tag relationships. + + :param graphs: A list of dependency graphs to train from. + :type: list(DependencyGraph) + """ + productions = [] + events = defaultdict(int) + tags = {} + for dg in graphs: + for node_index in range(1, len(dg.nodes)): + # children = dg.nodes[node_index]['deps'] + children = list( + chain.from_iterable(dg.nodes[node_index]["deps"].values()) + ) + + nr_left_children = dg.left_children(node_index) + nr_right_children = dg.right_children(node_index) + nr_children = nr_left_children + nr_right_children + for child_index in range( + 0 - (nr_left_children + 1), nr_right_children + 2 + ): + head_word = dg.nodes[node_index]["word"] + head_tag = dg.nodes[node_index]["tag"] + if head_word in tags: + tags[head_word].add(head_tag) + else: + tags[head_word] = {head_tag} + child = "STOP" + child_tag = "STOP" + prev_word = "START" + prev_tag = "START" + if child_index < 0: + array_index = child_index + nr_left_children + if array_index >= 0: + child = dg.nodes[children[array_index]]["word"] + child_tag = dg.nodes[children[array_index]]["tag"] + if child_index != -1: + prev_word = dg.nodes[children[array_index + 1]]["word"] + prev_tag = dg.nodes[children[array_index + 1]]["tag"] + if child != "STOP": + productions.append(DependencyProduction(head_word, [child])) + head_event = "(head ({} {}) (mods ({}, {}, {}) left))".format( + child, + child_tag, + prev_tag, + head_word, + head_tag, + ) + mod_event = "(mods ({}, {}, {}) left))".format( + prev_tag, + head_word, + head_tag, + ) + events[head_event] += 1 + events[mod_event] += 1 + elif child_index > 0: + array_index = child_index + nr_left_children - 1 + if array_index < nr_children: + child = dg.nodes[children[array_index]]["word"] + child_tag = dg.nodes[children[array_index]]["tag"] + if child_index != 1: + prev_word = dg.nodes[children[array_index - 1]]["word"] + prev_tag = dg.nodes[children[array_index - 1]]["tag"] + if child != "STOP": + productions.append(DependencyProduction(head_word, [child])) + head_event = "(head ({} {}) (mods ({}, {}, {}) right))".format( + child, + child_tag, + prev_tag, + head_word, + head_tag, + ) + mod_event = "(mods ({}, {}, {}) right))".format( + prev_tag, + head_word, + head_tag, + ) + events[head_event] += 1 + events[mod_event] += 1 + self._grammar = ProbabilisticDependencyGrammar(productions, events, tags) + + def compute_prob(self, dg): + """ + Computes the probability of a dependency graph based + on the parser's probability model (defined by the parser's + statistical dependency grammar). + + :param dg: A dependency graph to score. + :type dg: DependencyGraph + :return: The probability of the dependency graph. + :rtype: int + """ + prob = 1.0 + for node_index in range(1, len(dg.nodes)): + # children = dg.nodes[node_index]['deps'] + children = list(chain.from_iterable(dg.nodes[node_index]["deps"].values())) + + nr_left_children = dg.left_children(node_index) + nr_right_children = dg.right_children(node_index) + nr_children = nr_left_children + nr_right_children + for child_index in range(0 - (nr_left_children + 1), nr_right_children + 2): + head_word = dg.nodes[node_index]["word"] + head_tag = dg.nodes[node_index]["tag"] + child = "STOP" + child_tag = "STOP" + prev_word = "START" + prev_tag = "START" + if child_index < 0: + array_index = child_index + nr_left_children + if array_index >= 0: + child = dg.nodes[children[array_index]]["word"] + child_tag = dg.nodes[children[array_index]]["tag"] + if child_index != -1: + prev_word = dg.nodes[children[array_index + 1]]["word"] + prev_tag = dg.nodes[children[array_index + 1]]["tag"] + head_event = "(head ({} {}) (mods ({}, {}, {}) left))".format( + child, + child_tag, + prev_tag, + head_word, + head_tag, + ) + mod_event = "(mods ({}, {}, {}) left))".format( + prev_tag, + head_word, + head_tag, + ) + h_count = self._grammar._events[head_event] + m_count = self._grammar._events[mod_event] + + # If the grammar is not covered + if m_count != 0: + prob *= h_count / m_count + else: + prob = 0.00000001 # Very small number + + elif child_index > 0: + array_index = child_index + nr_left_children - 1 + if array_index < nr_children: + child = dg.nodes[children[array_index]]["word"] + child_tag = dg.nodes[children[array_index]]["tag"] + if child_index != 1: + prev_word = dg.nodes[children[array_index - 1]]["word"] + prev_tag = dg.nodes[children[array_index - 1]]["tag"] + head_event = "(head ({} {}) (mods ({}, {}, {}) right))".format( + child, + child_tag, + prev_tag, + head_word, + head_tag, + ) + mod_event = "(mods ({}, {}, {}) right))".format( + prev_tag, + head_word, + head_tag, + ) + h_count = self._grammar._events[head_event] + m_count = self._grammar._events[mod_event] + + if m_count != 0: + prob *= h_count / m_count + else: + prob = 0.00000001 # Very small number + + return prob + + +################################################################# +# Demos +################################################################# + + +def demo(): + projective_rule_parse_demo() + # arity_parse_demo() + projective_prob_parse_demo() + + +def projective_rule_parse_demo(): + """ + A demonstration showing the creation and use of a + ``DependencyGrammar`` to perform a projective dependency + parse. + """ + grammar = DependencyGrammar.fromstring( + """ + 'scratch' -> 'cats' | 'walls' + 'walls' -> 'the' + 'cats' -> 'the' + """ + ) + print(grammar) + pdp = ProjectiveDependencyParser(grammar) + trees = pdp.parse(["the", "cats", "scratch", "the", "walls"]) + for tree in trees: + print(tree) + + +def arity_parse_demo(): + """ + A demonstration showing the creation of a ``DependencyGrammar`` + in which a specific number of modifiers is listed for a given + head. This can further constrain the number of possible parses + created by a ``ProjectiveDependencyParser``. + """ + print() + print("A grammar with no arity constraints. Each DependencyProduction") + print("specifies a relationship between one head word and only one") + print("modifier word.") + grammar = DependencyGrammar.fromstring( + """ + 'fell' -> 'price' | 'stock' + 'price' -> 'of' | 'the' + 'of' -> 'stock' + 'stock' -> 'the' + """ + ) + print(grammar) + + print() + print("For the sentence 'The price of the stock fell', this grammar") + print("will produce the following three parses:") + pdp = ProjectiveDependencyParser(grammar) + trees = pdp.parse(["the", "price", "of", "the", "stock", "fell"]) + for tree in trees: + print(tree) + + print() + print("By contrast, the following grammar contains a ") + print("DependencyProduction that specifies a relationship") + print("between a single head word, 'price', and two modifier") + print("words, 'of' and 'the'.") + grammar = DependencyGrammar.fromstring( + """ + 'fell' -> 'price' | 'stock' + 'price' -> 'of' 'the' + 'of' -> 'stock' + 'stock' -> 'the' + """ + ) + print(grammar) + + print() + print( + "This constrains the number of possible parses to just one:" + ) # unimplemented, soon to replace + pdp = ProjectiveDependencyParser(grammar) + trees = pdp.parse(["the", "price", "of", "the", "stock", "fell"]) + for tree in trees: + print(tree) + + +def projective_prob_parse_demo(): + """ + A demo showing the training and use of a projective + dependency parser. + """ + from nltk.parse.dependencygraph import conll_data2 + + graphs = [DependencyGraph(entry) for entry in conll_data2.split("\n\n") if entry] + ppdp = ProbabilisticProjectiveDependencyParser() + print("Training Probabilistic Projective Dependency Parser...") + ppdp.train(graphs) + + sent = ["Cathy", "zag", "hen", "wild", "zwaaien", "."] + print("Parsing '", " ".join(sent), "'...") + print("Parse:") + for tree in ppdp.parse(sent): + print(tree) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/recursivedescent.py b/nltk/parse/recursivedescent.py new file mode 100644 index 0000000..1420fa5 --- /dev/null +++ b/nltk/parse/recursivedescent.py @@ -0,0 +1,684 @@ +# Natural Language Toolkit: Recursive Descent Parser +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from nltk.grammar import Nonterminal +from nltk.parse.api import ParserI +from nltk.tree import ImmutableTree, Tree + + +##////////////////////////////////////////////////////// +## Recursive Descent Parser +##////////////////////////////////////////////////////// +class RecursiveDescentParser(ParserI): + """ + A simple top-down CFG parser that parses texts by recursively + expanding the fringe of a Tree, and matching it against a + text. + + ``RecursiveDescentParser`` uses a list of tree locations called a + "frontier" to remember which subtrees have not yet been expanded + and which leaves have not yet been matched against the text. Each + tree location consists of a list of child indices specifying the + path from the root of the tree to a subtree or a leaf; see the + reference documentation for Tree for more information + about tree locations. + + When the parser begins parsing a text, it constructs a tree + containing only the start symbol, and a frontier containing the + location of the tree's root node. It then extends the tree to + cover the text, using the following recursive procedure: + + - If the frontier is empty, and the text is covered by the tree, + then return the tree as a possible parse. + - If the frontier is empty, and the text is not covered by the + tree, then return no parses. + - If the first element of the frontier is a subtree, then + use CFG productions to "expand" it. For each applicable + production, add the expanded subtree's children to the + frontier, and recursively find all parses that can be + generated by the new tree and frontier. + - If the first element of the frontier is a token, then "match" + it against the next token from the text. Remove the token + from the frontier, and recursively find all parses that can be + generated by the new tree and frontier. + + :see: ``nltk.grammar`` + """ + + def __init__(self, grammar, trace=0): + """ + Create a new ``RecursiveDescentParser``, that uses ``grammar`` + to parse texts. + + :type grammar: CFG + :param grammar: The grammar used to parse texts. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing + output. + """ + self._grammar = grammar + self._trace = trace + + def grammar(self): + return self._grammar + + def parse(self, tokens): + # Inherit docs from ParserI + + tokens = list(tokens) + self._grammar.check_coverage(tokens) + + # Start a recursive descent parse, with an initial tree + # containing just the start symbol. + start = self._grammar.start().symbol() + initial_tree = Tree(start, []) + frontier = [()] + if self._trace: + self._trace_start(initial_tree, frontier, tokens) + return self._parse(tokens, initial_tree, frontier) + + def _parse(self, remaining_text, tree, frontier): + """ + Recursively expand and match each elements of ``tree`` + specified by ``frontier``, to cover ``remaining_text``. Return + a list of all parses found. + + :return: An iterator of all parses that can be generated by + matching and expanding the elements of ``tree`` + specified by ``frontier``. + :rtype: iter(Tree) + :type tree: Tree + :param tree: A partial structure for the text that is + currently being parsed. The elements of ``tree`` + that are specified by ``frontier`` have not yet been + expanded or matched. + :type remaining_text: list(str) + :param remaining_text: The portion of the text that is not yet + covered by ``tree``. + :type frontier: list(tuple(int)) + :param frontier: A list of the locations within ``tree`` of + all subtrees that have not yet been expanded, and all + leaves that have not yet been matched. This list sorted + in left-to-right order of location within the tree. + """ + + # If the tree covers the text, and there's nothing left to + # expand, then we've found a complete parse; return it. + if len(remaining_text) == 0 and len(frontier) == 0: + if self._trace: + self._trace_succeed(tree, frontier) + yield tree + + # If there's still text, but nothing left to expand, we failed. + elif len(frontier) == 0: + if self._trace: + self._trace_backtrack(tree, frontier) + + # If the next element on the frontier is a tree, expand it. + elif isinstance(tree[frontier[0]], Tree): + yield from self._expand(remaining_text, tree, frontier) + + # If the next element on the frontier is a token, match it. + else: + yield from self._match(remaining_text, tree, frontier) + + def _match(self, rtext, tree, frontier): + """ + :rtype: iter(Tree) + :return: an iterator of all parses that can be generated by + matching the first element of ``frontier`` against the + first token in ``rtext``. In particular, if the first + element of ``frontier`` has the same type as the first + token in ``rtext``, then substitute the token into + ``tree``; and return all parses that can be generated by + matching and expanding the remaining elements of + ``frontier``. If the first element of ``frontier`` does not + have the same type as the first token in ``rtext``, then + return empty list. + + :type tree: Tree + :param tree: A partial structure for the text that is + currently being parsed. The elements of ``tree`` + that are specified by ``frontier`` have not yet been + expanded or matched. + :type rtext: list(str) + :param rtext: The portion of the text that is not yet + covered by ``tree``. + :type frontier: list of tuple of int + :param frontier: A list of the locations within ``tree`` of + all subtrees that have not yet been expanded, and all + leaves that have not yet been matched. + """ + + tree_leaf = tree[frontier[0]] + if len(rtext) > 0 and tree_leaf == rtext[0]: + # If it's a terminal that matches rtext[0], then substitute + # in the token, and continue parsing. + newtree = tree.copy(deep=True) + newtree[frontier[0]] = rtext[0] + if self._trace: + self._trace_match(newtree, frontier[1:], rtext[0]) + yield from self._parse(rtext[1:], newtree, frontier[1:]) + else: + # If it's a non-matching terminal, fail. + if self._trace: + self._trace_backtrack(tree, frontier, rtext[:1]) + + def _expand(self, remaining_text, tree, frontier, production=None): + """ + :rtype: iter(Tree) + :return: An iterator of all parses that can be generated by + expanding the first element of ``frontier`` with + ``production``. In particular, if the first element of + ``frontier`` is a subtree whose node type is equal to + ``production``'s left hand side, then add a child to that + subtree for each element of ``production``'s right hand + side; and return all parses that can be generated by + matching and expanding the remaining elements of + ``frontier``. If the first element of ``frontier`` is not a + subtree whose node type is equal to ``production``'s left + hand side, then return an empty list. If ``production`` is + not specified, then return a list of all parses that can + be generated by expanding the first element of ``frontier`` + with *any* CFG production. + + :type tree: Tree + :param tree: A partial structure for the text that is + currently being parsed. The elements of ``tree`` + that are specified by ``frontier`` have not yet been + expanded or matched. + :type remaining_text: list(str) + :param remaining_text: The portion of the text that is not yet + covered by ``tree``. + :type frontier: list(tuple(int)) + :param frontier: A list of the locations within ``tree`` of + all subtrees that have not yet been expanded, and all + leaves that have not yet been matched. + """ + + if production is None: + productions = self._grammar.productions() + else: + productions = [production] + + for production in productions: + lhs = production.lhs().symbol() + if lhs == tree[frontier[0]].label(): + subtree = self._production_to_tree(production) + if frontier[0] == (): + newtree = subtree + else: + newtree = tree.copy(deep=True) + newtree[frontier[0]] = subtree + new_frontier = [ + frontier[0] + (i,) for i in range(len(production.rhs())) + ] + if self._trace: + self._trace_expand(newtree, new_frontier, production) + yield from self._parse( + remaining_text, newtree, new_frontier + frontier[1:] + ) + + def _production_to_tree(self, production): + """ + :rtype: Tree + :return: The Tree that is licensed by ``production``. + In particular, given the production ``[lhs -> elt[1] ... elt[n]]`` + return a tree that has a node ``lhs.symbol``, and + ``n`` children. For each nonterminal element + ``elt[i]`` in the production, the tree token has a + childless subtree with node value ``elt[i].symbol``; and + for each terminal element ``elt[j]``, the tree token has + a leaf token with type ``elt[j]``. + + :param production: The CFG production that licenses the tree + token that should be returned. + :type production: Production + """ + children = [] + for elt in production.rhs(): + if isinstance(elt, Nonterminal): + children.append(Tree(elt.symbol(), [])) + else: + # This will be matched. + children.append(elt) + return Tree(production.lhs().symbol(), children) + + def trace(self, trace=2): + """ + Set the level of tracing output that should be generated when + parsing a text. + + :type trace: int + :param trace: The trace level. A trace level of ``0`` will + generate no tracing output; and higher trace levels will + produce more verbose tracing output. + :rtype: None + """ + self._trace = trace + + def _trace_fringe(self, tree, treeloc=None): + """ + Print trace output displaying the fringe of ``tree``. The + fringe of ``tree`` consists of all of its leaves and all of + its childless subtrees. + + :rtype: None + """ + + if treeloc == (): + print("*", end=" ") + if isinstance(tree, Tree): + if len(tree) == 0: + print(repr(Nonterminal(tree.label())), end=" ") + for i in range(len(tree)): + if treeloc is not None and i == treeloc[0]: + self._trace_fringe(tree[i], treeloc[1:]) + else: + self._trace_fringe(tree[i]) + else: + print(repr(tree), end=" ") + + def _trace_tree(self, tree, frontier, operation): + """ + Print trace output displaying the parser's current state. + + :param operation: A character identifying the operation that + generated the current state. + :rtype: None + """ + if self._trace == 2: + print(" %c [" % operation, end=" ") + else: + print(" [", end=" ") + if len(frontier) > 0: + self._trace_fringe(tree, frontier[0]) + else: + self._trace_fringe(tree) + print("]") + + def _trace_start(self, tree, frontier, text): + print("Parsing %r" % " ".join(text)) + if self._trace > 2: + print("Start:") + if self._trace > 1: + self._trace_tree(tree, frontier, " ") + + def _trace_expand(self, tree, frontier, production): + if self._trace > 2: + print("Expand: %s" % production) + if self._trace > 1: + self._trace_tree(tree, frontier, "E") + + def _trace_match(self, tree, frontier, tok): + if self._trace > 2: + print("Match: %r" % tok) + if self._trace > 1: + self._trace_tree(tree, frontier, "M") + + def _trace_succeed(self, tree, frontier): + if self._trace > 2: + print("GOOD PARSE:") + if self._trace == 1: + print("Found a parse:\n%s" % tree) + if self._trace > 1: + self._trace_tree(tree, frontier, "+") + + def _trace_backtrack(self, tree, frontier, toks=None): + if self._trace > 2: + if toks: + print("Backtrack: %r match failed" % toks[0]) + else: + print("Backtrack") + + +##////////////////////////////////////////////////////// +## Stepping Recursive Descent Parser +##////////////////////////////////////////////////////// +class SteppingRecursiveDescentParser(RecursiveDescentParser): + """ + A ``RecursiveDescentParser`` that allows you to step through the + parsing process, performing a single operation at a time. + + The ``initialize`` method is used to start parsing a text. + ``expand`` expands the first element on the frontier using a single + CFG production, and ``match`` matches the first element on the + frontier against the next text token. ``backtrack`` undoes the most + recent expand or match operation. ``step`` performs a single + expand, match, or backtrack operation. ``parses`` returns the set + of parses that have been found by the parser. + + :ivar _history: A list of ``(rtext, tree, frontier)`` tripples, + containing the previous states of the parser. This history is + used to implement the ``backtrack`` operation. + :ivar _tried_e: A record of all productions that have been tried + for a given tree. This record is used by ``expand`` to perform + the next untried production. + :ivar _tried_m: A record of what tokens have been matched for a + given tree. This record is used by ``step`` to decide whether + or not to match a token. + :see: ``nltk.grammar`` + """ + + def __init__(self, grammar, trace=0): + super().__init__(grammar, trace) + self._rtext = None + self._tree = None + self._frontier = [()] + self._tried_e = {} + self._tried_m = {} + self._history = [] + self._parses = [] + + # [XX] TEMPORARY HACK WARNING! This should be replaced with + # something nicer when we get the chance. + def _freeze(self, tree): + c = tree.copy() + # for pos in c.treepositions('leaves'): + # c[pos] = c[pos].freeze() + return ImmutableTree.convert(c) + + def parse(self, tokens): + tokens = list(tokens) + self.initialize(tokens) + while self.step() is not None: + pass + return self.parses() + + def initialize(self, tokens): + """ + Start parsing a given text. This sets the parser's tree to + the start symbol, its frontier to the root node, and its + remaining text to ``token['SUBTOKENS']``. + """ + + self._rtext = tokens + start = self._grammar.start().symbol() + self._tree = Tree(start, []) + self._frontier = [()] + self._tried_e = {} + self._tried_m = {} + self._history = [] + self._parses = [] + if self._trace: + self._trace_start(self._tree, self._frontier, self._rtext) + + def remaining_text(self): + """ + :return: The portion of the text that is not yet covered by the + tree. + :rtype: list(str) + """ + return self._rtext + + def frontier(self): + """ + :return: A list of the tree locations of all subtrees that + have not yet been expanded, and all leaves that have not + yet been matched. + :rtype: list(tuple(int)) + """ + return self._frontier + + def tree(self): + """ + :return: A partial structure for the text that is + currently being parsed. The elements specified by the + frontier have not yet been expanded or matched. + :rtype: Tree + """ + return self._tree + + def step(self): + """ + Perform a single parsing operation. If an untried match is + possible, then perform the match, and return the matched + token. If an untried expansion is possible, then perform the + expansion, and return the production that it is based on. If + backtracking is possible, then backtrack, and return True. + Otherwise, return None. + + :return: None if no operation was performed; a token if a match + was performed; a production if an expansion was performed; + and True if a backtrack operation was performed. + :rtype: Production or String or bool + """ + # Try matching (if we haven't already) + if self.untried_match(): + token = self.match() + if token is not None: + return token + + # Try expanding. + production = self.expand() + if production is not None: + return production + + # Try backtracking + if self.backtrack(): + self._trace_backtrack(self._tree, self._frontier) + return True + + # Nothing left to do. + return None + + def expand(self, production=None): + """ + Expand the first element of the frontier. In particular, if + the first element of the frontier is a subtree whose node type + is equal to ``production``'s left hand side, then add a child + to that subtree for each element of ``production``'s right hand + side. If ``production`` is not specified, then use the first + untried expandable production. If all expandable productions + have been tried, do nothing. + + :return: The production used to expand the frontier, if an + expansion was performed. If no expansion was performed, + return None. + :rtype: Production or None + """ + + # Make sure we *can* expand. + if len(self._frontier) == 0: + return None + if not isinstance(self._tree[self._frontier[0]], Tree): + return None + + # If they didn't specify a production, check all untried ones. + if production is None: + productions = self.untried_expandable_productions() + else: + productions = [production] + + parses = [] + for prod in productions: + # Record that we've tried this production now. + self._tried_e.setdefault(self._freeze(self._tree), []).append(prod) + + # Try expanding. + for _result in self._expand(self._rtext, self._tree, self._frontier, prod): + return prod + + # We didn't expand anything. + return None + + def match(self): + """ + Match the first element of the frontier. In particular, if + the first element of the frontier has the same type as the + next text token, then substitute the text token into the tree. + + :return: The token matched, if a match operation was + performed. If no match was performed, return None + :rtype: str or None + """ + + # Record that we've tried matching this token. + tok = self._rtext[0] + self._tried_m.setdefault(self._freeze(self._tree), []).append(tok) + + # Make sure we *can* match. + if len(self._frontier) == 0: + return None + if isinstance(self._tree[self._frontier[0]], Tree): + return None + + for _result in self._match(self._rtext, self._tree, self._frontier): + # Return the token we just matched. + return self._history[-1][0][0] + return None + + def backtrack(self): + """ + Return the parser to its state before the most recent + match or expand operation. Calling ``undo`` repeatedly return + the parser to successively earlier states. If no match or + expand operations have been performed, ``undo`` will make no + changes. + + :return: true if an operation was successfully undone. + :rtype: bool + """ + if len(self._history) == 0: + return False + (self._rtext, self._tree, self._frontier) = self._history.pop() + return True + + def expandable_productions(self): + """ + :return: A list of all the productions for which expansions + are available for the current parser state. + :rtype: list(Production) + """ + # Make sure we *can* expand. + if len(self._frontier) == 0: + return [] + frontier_child = self._tree[self._frontier[0]] + if len(self._frontier) == 0 or not isinstance(frontier_child, Tree): + return [] + + return [ + p + for p in self._grammar.productions() + if p.lhs().symbol() == frontier_child.label() + ] + + def untried_expandable_productions(self): + """ + :return: A list of all the untried productions for which + expansions are available for the current parser state. + :rtype: list(Production) + """ + + tried_expansions = self._tried_e.get(self._freeze(self._tree), []) + return [p for p in self.expandable_productions() if p not in tried_expansions] + + def untried_match(self): + """ + :return: Whether the first element of the frontier is a token + that has not yet been matched. + :rtype: bool + """ + + if len(self._rtext) == 0: + return False + tried_matches = self._tried_m.get(self._freeze(self._tree), []) + return self._rtext[0] not in tried_matches + + def currently_complete(self): + """ + :return: Whether the parser's current state represents a + complete parse. + :rtype: bool + """ + return len(self._frontier) == 0 and len(self._rtext) == 0 + + def _parse(self, remaining_text, tree, frontier): + """ + A stub version of ``_parse`` that sets the parsers current + state to the given arguments. In ``RecursiveDescentParser``, + the ``_parse`` method is used to recursively continue parsing a + text. ``SteppingRecursiveDescentParser`` overrides it to + capture these recursive calls. It records the parser's old + state in the history (to allow for backtracking), and updates + the parser's new state using the given arguments. Finally, it + returns ``[1]``, which is used by ``match`` and ``expand`` to + detect whether their operations were successful. + + :return: ``[1]`` + :rtype: list of int + """ + self._history.append((self._rtext, self._tree, self._frontier)) + self._rtext = remaining_text + self._tree = tree + self._frontier = frontier + + # Is it a good parse? If so, record it. + if len(frontier) == 0 and len(remaining_text) == 0: + self._parses.append(tree) + self._trace_succeed(self._tree, self._frontier) + + return [1] + + def parses(self): + """ + :return: An iterator of the parses that have been found by this + parser so far. + :rtype: list of Tree + """ + return iter(self._parses) + + def set_grammar(self, grammar): + """ + Change the grammar used to parse texts. + + :param grammar: The new grammar. + :type grammar: CFG + """ + self._grammar = grammar + + +##////////////////////////////////////////////////////// +## Demonstration Code +##////////////////////////////////////////////////////// + + +def demo(): + """ + A demonstration of the recursive descent parser. + """ + + from nltk import CFG, parse + + grammar = CFG.fromstring( + """ + S -> NP VP + NP -> Det N | Det N PP + VP -> V NP | V NP PP + PP -> P NP + NP -> 'I' + N -> 'man' | 'park' | 'telescope' | 'dog' + Det -> 'the' | 'a' + P -> 'in' | 'with' + V -> 'saw' + """ + ) + + for prod in grammar.productions(): + print(prod) + + sent = "I saw a man in the park".split() + parser = parse.RecursiveDescentParser(grammar, trace=2) + for p in parser.parse(sent): + print(p) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/shiftreduce.py b/nltk/parse/shiftreduce.py new file mode 100644 index 0000000..ae2be5a --- /dev/null +++ b/nltk/parse/shiftreduce.py @@ -0,0 +1,478 @@ +# Natural Language Toolkit: Shift-Reduce Parser +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from nltk.grammar import Nonterminal +from nltk.parse.api import ParserI +from nltk.tree import Tree + + +##////////////////////////////////////////////////////// +## Shift/Reduce Parser +##////////////////////////////////////////////////////// +class ShiftReduceParser(ParserI): + """ + A simple bottom-up CFG parser that uses two operations, "shift" + and "reduce", to find a single parse for a text. + + ``ShiftReduceParser`` maintains a stack, which records the + structure of a portion of the text. This stack is a list of + strings and Trees that collectively cover a portion of + the text. For example, while parsing the sentence "the dog saw + the man" with a typical grammar, ``ShiftReduceParser`` will produce + the following stack, which covers "the dog saw":: + + [(NP: (Det: 'the') (N: 'dog')), (V: 'saw')] + + ``ShiftReduceParser`` attempts to extend the stack to cover the + entire text, and to combine the stack elements into a single tree, + producing a complete parse for the sentence. + + Initially, the stack is empty. It is extended to cover the text, + from left to right, by repeatedly applying two operations: + + - "shift" moves a token from the beginning of the text to the + end of the stack. + - "reduce" uses a CFG production to combine the rightmost stack + elements into a single Tree. + + Often, more than one operation can be performed on a given stack. + In this case, ``ShiftReduceParser`` uses the following heuristics + to decide which operation to perform: + + - Only shift if no reductions are available. + - If multiple reductions are available, then apply the reduction + whose CFG production is listed earliest in the grammar. + + Note that these heuristics are not guaranteed to choose an + operation that leads to a parse of the text. Also, if multiple + parses exists, ``ShiftReduceParser`` will return at most one of + them. + + :see: ``nltk.grammar`` + """ + + def __init__(self, grammar, trace=0): + """ + Create a new ``ShiftReduceParser``, that uses ``grammar`` to + parse texts. + + :type grammar: Grammar + :param grammar: The grammar used to parse texts. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing + output. + """ + self._grammar = grammar + self._trace = trace + self._check_grammar() + + def grammar(self): + return self._grammar + + def parse(self, tokens): + tokens = list(tokens) + self._grammar.check_coverage(tokens) + + # initialize the stack. + stack = [] + remaining_text = tokens + + # Trace output. + if self._trace: + print("Parsing %r" % " ".join(tokens)) + self._trace_stack(stack, remaining_text) + + # iterate through the text, pushing the token onto + # the stack, then reducing the stack. + while len(remaining_text) > 0: + self._shift(stack, remaining_text) + while self._reduce(stack, remaining_text): + pass + + # Did we reduce everything? + if len(stack) == 1: + # Did we end up with the right category? + if stack[0].label() == self._grammar.start().symbol(): + yield stack[0] + + def _shift(self, stack, remaining_text): + """ + Move a token from the beginning of ``remaining_text`` to the + end of ``stack``. + + :type stack: list(str and Tree) + :param stack: A list of strings and Trees, encoding + the structure of the text that has been parsed so far. + :type remaining_text: list(str) + :param remaining_text: The portion of the text that is not yet + covered by ``stack``. + :rtype: None + """ + stack.append(remaining_text[0]) + remaining_text.remove(remaining_text[0]) + if self._trace: + self._trace_shift(stack, remaining_text) + + def _match_rhs(self, rhs, rightmost_stack): + """ + :rtype: bool + :return: true if the right hand side of a CFG production + matches the rightmost elements of the stack. ``rhs`` + matches ``rightmost_stack`` if they are the same length, + and each element of ``rhs`` matches the corresponding + element of ``rightmost_stack``. A nonterminal element of + ``rhs`` matches any Tree whose node value is equal + to the nonterminal's symbol. A terminal element of ``rhs`` + matches any string whose type is equal to the terminal. + :type rhs: list(terminal and Nonterminal) + :param rhs: The right hand side of a CFG production. + :type rightmost_stack: list(string and Tree) + :param rightmost_stack: The rightmost elements of the parser's + stack. + """ + + if len(rightmost_stack) != len(rhs): + return False + for i in range(len(rightmost_stack)): + if isinstance(rightmost_stack[i], Tree): + if not isinstance(rhs[i], Nonterminal): + return False + if rightmost_stack[i].label() != rhs[i].symbol(): + return False + else: + if isinstance(rhs[i], Nonterminal): + return False + if rightmost_stack[i] != rhs[i]: + return False + return True + + def _reduce(self, stack, remaining_text, production=None): + """ + Find a CFG production whose right hand side matches the + rightmost stack elements; and combine those stack elements + into a single Tree, with the node specified by the + production's left-hand side. If more than one CFG production + matches the stack, then use the production that is listed + earliest in the grammar. The new Tree replaces the + elements in the stack. + + :rtype: Production or None + :return: If a reduction is performed, then return the CFG + production that the reduction is based on; otherwise, + return false. + :type stack: list(string and Tree) + :param stack: A list of strings and Trees, encoding + the structure of the text that has been parsed so far. + :type remaining_text: list(str) + :param remaining_text: The portion of the text that is not yet + covered by ``stack``. + """ + if production is None: + productions = self._grammar.productions() + else: + productions = [production] + + # Try each production, in order. + for production in productions: + rhslen = len(production.rhs()) + + # check if the RHS of a production matches the top of the stack + if self._match_rhs(production.rhs(), stack[-rhslen:]): + # combine the tree to reflect the reduction + tree = Tree(production.lhs().symbol(), stack[-rhslen:]) + stack[-rhslen:] = [tree] + + # We reduced something + if self._trace: + self._trace_reduce(stack, production, remaining_text) + return production + + # We didn't reduce anything + return None + + def trace(self, trace=2): + """ + Set the level of tracing output that should be generated when + parsing a text. + + :type trace: int + :param trace: The trace level. A trace level of ``0`` will + generate no tracing output; and higher trace levels will + produce more verbose tracing output. + :rtype: None + """ + # 1: just show shifts. + # 2: show shifts & reduces + # 3: display which tokens & productions are shifed/reduced + self._trace = trace + + def _trace_stack(self, stack, remaining_text, marker=" "): + """ + Print trace output displaying the given stack and text. + + :rtype: None + :param marker: A character that is printed to the left of the + stack. This is used with trace level 2 to print 'S' + before shifted stacks and 'R' before reduced stacks. + """ + s = " " + marker + " [ " + for elt in stack: + if isinstance(elt, Tree): + s += repr(Nonterminal(elt.label())) + " " + else: + s += repr(elt) + " " + s += "* " + " ".join(remaining_text) + "]" + print(s) + + def _trace_shift(self, stack, remaining_text): + """ + Print trace output displaying that a token has been shifted. + + :rtype: None + """ + if self._trace > 2: + print("Shift %r:" % stack[-1]) + if self._trace == 2: + self._trace_stack(stack, remaining_text, "S") + elif self._trace > 0: + self._trace_stack(stack, remaining_text) + + def _trace_reduce(self, stack, production, remaining_text): + """ + Print trace output displaying that ``production`` was used to + reduce ``stack``. + + :rtype: None + """ + if self._trace > 2: + rhs = " ".join(production.rhs()) + print(f"Reduce {production.lhs()!r} <- {rhs}") + if self._trace == 2: + self._trace_stack(stack, remaining_text, "R") + elif self._trace > 1: + self._trace_stack(stack, remaining_text) + + def _check_grammar(self): + """ + Check to make sure that all of the CFG productions are + potentially useful. If any productions can never be used, + then print a warning. + + :rtype: None + """ + productions = self._grammar.productions() + + # Any production whose RHS is an extension of another production's RHS + # will never be used. + for i in range(len(productions)): + for j in range(i + 1, len(productions)): + rhs1 = productions[i].rhs() + rhs2 = productions[j].rhs() + if rhs1[: len(rhs2)] == rhs2: + print("Warning: %r will never be used" % productions[i]) + + +##////////////////////////////////////////////////////// +## Stepping Shift/Reduce Parser +##////////////////////////////////////////////////////// +class SteppingShiftReduceParser(ShiftReduceParser): + """ + A ``ShiftReduceParser`` that allows you to setp through the parsing + process, performing a single operation at a time. It also allows + you to change the parser's grammar midway through parsing a text. + + The ``initialize`` method is used to start parsing a text. + ``shift`` performs a single shift operation, and ``reduce`` performs + a single reduce operation. ``step`` will perform a single reduce + operation if possible; otherwise, it will perform a single shift + operation. ``parses`` returns the set of parses that have been + found by the parser. + + :ivar _history: A list of ``(stack, remaining_text)`` pairs, + containing all of the previous states of the parser. This + history is used to implement the ``undo`` operation. + :see: ``nltk.grammar`` + """ + + def __init__(self, grammar, trace=0): + super().__init__(grammar, trace) + self._stack = None + self._remaining_text = None + self._history = [] + + def parse(self, tokens): + tokens = list(tokens) + self.initialize(tokens) + while self.step(): + pass + return self.parses() + + def stack(self): + """ + :return: The parser's stack. + :rtype: list(str and Tree) + """ + return self._stack + + def remaining_text(self): + """ + :return: The portion of the text that is not yet covered by the + stack. + :rtype: list(str) + """ + return self._remaining_text + + def initialize(self, tokens): + """ + Start parsing a given text. This sets the parser's stack to + ``[]`` and sets its remaining text to ``tokens``. + """ + self._stack = [] + self._remaining_text = tokens + self._history = [] + + def step(self): + """ + Perform a single parsing operation. If a reduction is + possible, then perform that reduction, and return the + production that it is based on. Otherwise, if a shift is + possible, then perform it, and return True. Otherwise, + return False. + + :return: False if no operation was performed; True if a shift was + performed; and the CFG production used to reduce if a + reduction was performed. + :rtype: Production or bool + """ + return self.reduce() or self.shift() + + def shift(self): + """ + Move a token from the beginning of the remaining text to the + end of the stack. If there are no more tokens in the + remaining text, then do nothing. + + :return: True if the shift operation was successful. + :rtype: bool + """ + if len(self._remaining_text) == 0: + return False + self._history.append((self._stack[:], self._remaining_text[:])) + self._shift(self._stack, self._remaining_text) + return True + + def reduce(self, production=None): + """ + Use ``production`` to combine the rightmost stack elements into + a single Tree. If ``production`` does not match the + rightmost stack elements, then do nothing. + + :return: The production used to reduce the stack, if a + reduction was performed. If no reduction was performed, + return None. + + :rtype: Production or None + """ + self._history.append((self._stack[:], self._remaining_text[:])) + return_val = self._reduce(self._stack, self._remaining_text, production) + + if not return_val: + self._history.pop() + return return_val + + def undo(self): + """ + Return the parser to its state before the most recent + shift or reduce operation. Calling ``undo`` repeatedly return + the parser to successively earlier states. If no shift or + reduce operations have been performed, ``undo`` will make no + changes. + + :return: true if an operation was successfully undone. + :rtype: bool + """ + if len(self._history) == 0: + return False + (self._stack, self._remaining_text) = self._history.pop() + return True + + def reducible_productions(self): + """ + :return: A list of the productions for which reductions are + available for the current parser state. + :rtype: list(Production) + """ + productions = [] + for production in self._grammar.productions(): + rhslen = len(production.rhs()) + if self._match_rhs(production.rhs(), self._stack[-rhslen:]): + productions.append(production) + return productions + + def parses(self): + """ + :return: An iterator of the parses that have been found by this + parser so far. + :rtype: iter(Tree) + """ + if ( + len(self._remaining_text) == 0 + and len(self._stack) == 1 + and self._stack[0].label() == self._grammar.start().symbol() + ): + yield self._stack[0] + + # copied from nltk.parser + + def set_grammar(self, grammar): + """ + Change the grammar used to parse texts. + + :param grammar: The new grammar. + :type grammar: CFG + """ + self._grammar = grammar + + +##////////////////////////////////////////////////////// +## Demonstration Code +##////////////////////////////////////////////////////// + + +def demo(): + """ + A demonstration of the shift-reduce parser. + """ + + from nltk import CFG, parse + + grammar = CFG.fromstring( + """ + S -> NP VP + NP -> Det N | Det N PP + VP -> V NP | V NP PP + PP -> P NP + NP -> 'I' + N -> 'man' | 'park' | 'telescope' | 'dog' + Det -> 'the' | 'a' + P -> 'in' | 'with' + V -> 'saw' + """ + ) + + sent = "I saw a man in the park".split() + + parser = parse.ShiftReduceParser(grammar, trace=2) + for p in parser.parse(sent): + print(p) + + +if __name__ == "__main__": + demo() diff --git a/nltk/parse/stanford.py b/nltk/parse/stanford.py new file mode 100644 index 0000000..b53f2e9 --- /dev/null +++ b/nltk/parse/stanford.py @@ -0,0 +1,471 @@ +# Natural Language Toolkit: Interface to the Stanford Parser +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Xu +# +# URL: +# For license information, see LICENSE.TXT + +import os +import tempfile +import warnings +from subprocess import PIPE + +from nltk.internals import find_jar_iter, find_jars_within_path, java +from nltk.parse.api import ParserI +from nltk.parse.dependencygraph import DependencyGraph +from nltk.tree import Tree + +_stanford_url = "https://nlp.stanford.edu/software/lex-parser.shtml" + + +class GenericStanfordParser(ParserI): + """Interface to the Stanford Parser""" + + _MODEL_JAR_PATTERN = r"stanford-parser-(\d+)(\.(\d+))+-models\.jar" + _JAR = r"stanford-parser\.jar" + _MAIN_CLASS = "edu.stanford.nlp.parser.lexparser.LexicalizedParser" + + _USE_STDIN = False + _DOUBLE_SPACED_OUTPUT = False + + def __init__( + self, + path_to_jar=None, + path_to_models_jar=None, + model_path="edu/stanford/nlp/models/lexparser/englishPCFG.ser.gz", + encoding="utf8", + verbose=False, + java_options="-mx4g", + corenlp_options="", + ): + # find the most recent code and model jar + stanford_jar = max( + find_jar_iter( + self._JAR, + path_to_jar, + env_vars=("STANFORD_PARSER", "STANFORD_CORENLP"), + searchpath=(), + url=_stanford_url, + verbose=verbose, + is_regex=True, + ), + key=lambda model_path: os.path.dirname(model_path), + ) + + model_jar = max( + find_jar_iter( + self._MODEL_JAR_PATTERN, + path_to_models_jar, + env_vars=("STANFORD_MODELS", "STANFORD_CORENLP"), + searchpath=(), + url=_stanford_url, + verbose=verbose, + is_regex=True, + ), + key=lambda model_path: os.path.dirname(model_path), + ) + + # self._classpath = (stanford_jar, model_jar) + + # Adding logging jar files to classpath + stanford_dir = os.path.split(stanford_jar)[0] + self._classpath = tuple([model_jar] + find_jars_within_path(stanford_dir)) + + self.model_path = model_path + self._encoding = encoding + self.corenlp_options = corenlp_options + self.java_options = java_options + + def _parse_trees_output(self, output_): + res = [] + cur_lines = [] + cur_trees = [] + blank = False + for line in output_.splitlines(False): + if line == "": + if blank: + res.append(iter(cur_trees)) + cur_trees = [] + blank = False + elif self._DOUBLE_SPACED_OUTPUT: + cur_trees.append(self._make_tree("\n".join(cur_lines))) + cur_lines = [] + blank = True + else: + res.append(iter([self._make_tree("\n".join(cur_lines))])) + cur_lines = [] + else: + cur_lines.append(line) + blank = False + return iter(res) + + def parse_sents(self, sentences, verbose=False): + """ + Use StanfordParser to parse multiple sentences. Takes multiple sentences as a + list where each sentence is a list of words. + Each sentence will be automatically tagged with this StanfordParser instance's + tagger. + If whitespaces exists inside a token, then the token will be treated as + separate tokens. + + :param sentences: Input sentences to parse + :type sentences: list(list(str)) + :rtype: iter(iter(Tree)) + """ + cmd = [ + self._MAIN_CLASS, + "-model", + self.model_path, + "-sentences", + "newline", + "-outputFormat", + self._OUTPUT_FORMAT, + "-tokenized", + "-escaper", + "edu.stanford.nlp.process.PTBEscapingProcessor", + ] + return self._parse_trees_output( + self._execute( + cmd, "\n".join(" ".join(sentence) for sentence in sentences), verbose + ) + ) + + def raw_parse(self, sentence, verbose=False): + """ + Use StanfordParser to parse a sentence. Takes a sentence as a string; + before parsing, it will be automatically tokenized and tagged by + the Stanford Parser. + + :param sentence: Input sentence to parse + :type sentence: str + :rtype: iter(Tree) + """ + return next(self.raw_parse_sents([sentence], verbose)) + + def raw_parse_sents(self, sentences, verbose=False): + """ + Use StanfordParser to parse multiple sentences. Takes multiple sentences as a + list of strings. + Each sentence will be automatically tokenized and tagged by the Stanford Parser. + + :param sentences: Input sentences to parse + :type sentences: list(str) + :rtype: iter(iter(Tree)) + """ + cmd = [ + self._MAIN_CLASS, + "-model", + self.model_path, + "-sentences", + "newline", + "-outputFormat", + self._OUTPUT_FORMAT, + ] + return self._parse_trees_output( + self._execute(cmd, "\n".join(sentences), verbose) + ) + + def tagged_parse(self, sentence, verbose=False): + """ + Use StanfordParser to parse a sentence. Takes a sentence as a list of + (word, tag) tuples; the sentence must have already been tokenized and + tagged. + + :param sentence: Input sentence to parse + :type sentence: list(tuple(str, str)) + :rtype: iter(Tree) + """ + return next(self.tagged_parse_sents([sentence], verbose)) + + def tagged_parse_sents(self, sentences, verbose=False): + """ + Use StanfordParser to parse multiple sentences. Takes multiple sentences + where each sentence is a list of (word, tag) tuples. + The sentences must have already been tokenized and tagged. + + :param sentences: Input sentences to parse + :type sentences: list(list(tuple(str, str))) + :rtype: iter(iter(Tree)) + """ + tag_separator = "/" + cmd = [ + self._MAIN_CLASS, + "-model", + self.model_path, + "-sentences", + "newline", + "-outputFormat", + self._OUTPUT_FORMAT, + "-tokenized", + "-tagSeparator", + tag_separator, + "-tokenizerFactory", + "edu.stanford.nlp.process.WhitespaceTokenizer", + "-tokenizerMethod", + "newCoreLabelTokenizerFactory", + ] + # We don't need to escape slashes as "splitting is done on the last instance of the character in the token" + return self._parse_trees_output( + self._execute( + cmd, + "\n".join( + " ".join(tag_separator.join(tagged) for tagged in sentence) + for sentence in sentences + ), + verbose, + ) + ) + + def _execute(self, cmd, input_, verbose=False): + encoding = self._encoding + cmd.extend(["-encoding", encoding]) + if self.corenlp_options: + cmd.extend(self.corenlp_options.split()) + + # Windows is incompatible with NamedTemporaryFile() without passing in delete=False. + input_file_name = None + java_succeeded = False + try: + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as input_file: + input_file_name = input_file.name + # Write the actual sentences to the temporary input file + if isinstance(input_, str) and encoding: + input_ = input_.encode(encoding) + input_file.write(input_) + input_file.flush() + + # Run the tagger and get the output. + if self._USE_STDIN: + input_file.seek(0) + stdout, stderr = java( + cmd, + classpath=self._classpath, + stdin=input_file, + stdout=PIPE, + stderr=PIPE, + options=self.java_options, + ) + else: + cmd.append(input_file_name) + stdout, stderr = java( + cmd, + classpath=self._classpath, + stdout=PIPE, + stderr=PIPE, + options=self.java_options, + ) + + stdout = stdout.replace(b"\xc2\xa0", b" ") + stdout = stdout.replace(b"\x00\xa0", b" ") + stdout = stdout.decode(encoding) + java_succeeded = True + finally: + if input_file_name: + try: + os.unlink(input_file_name) + except FileNotFoundError: + pass + except OSError: + if java_succeeded: + raise + + return stdout + + +class StanfordParser(GenericStanfordParser): + """ + >>> parser=StanfordParser( + ... model_path="edu/stanford/nlp/models/lexparser/englishPCFG.ser.gz" + ... ) # doctest: +SKIP + + >>> list(parser.raw_parse("the quick brown fox jumps over the lazy dog")) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('ROOT', [Tree('NP', [Tree('NP', [Tree('DT', ['the']), Tree('JJ', ['quick']), Tree('JJ', ['brown']), + Tree('NN', ['fox'])]), Tree('NP', [Tree('NP', [Tree('NNS', ['jumps'])]), Tree('PP', [Tree('IN', ['over']), + Tree('NP', [Tree('DT', ['the']), Tree('JJ', ['lazy']), Tree('NN', ['dog'])])])])])])] + + >>> sum([list(dep_graphs) for dep_graphs in parser.raw_parse_sents(( + ... "the quick brown fox jumps over the lazy dog", + ... "the quick grey wolf jumps over the lazy fox" + ... ))], []) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('ROOT', [Tree('NP', [Tree('NP', [Tree('DT', ['the']), Tree('JJ', ['quick']), Tree('JJ', ['brown']), + Tree('NN', ['fox'])]), Tree('NP', [Tree('NP', [Tree('NNS', ['jumps'])]), Tree('PP', [Tree('IN', ['over']), + Tree('NP', [Tree('DT', ['the']), Tree('JJ', ['lazy']), Tree('NN', ['dog'])])])])])]), Tree('ROOT', [Tree('NP', + [Tree('NP', [Tree('DT', ['the']), Tree('JJ', ['quick']), Tree('JJ', ['grey']), Tree('NN', ['wolf'])]), Tree('NP', + [Tree('NP', [Tree('NNS', ['jumps'])]), Tree('PP', [Tree('IN', ['over']), Tree('NP', [Tree('DT', ['the']), + Tree('JJ', ['lazy']), Tree('NN', ['fox'])])])])])])] + + >>> sum([list(dep_graphs) for dep_graphs in parser.parse_sents(( + ... "I 'm a dog".split(), + ... "This is my friends ' cat ( the tabby )".split(), + ... ))], []) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('ROOT', [Tree('S', [Tree('NP', [Tree('PRP', ['I'])]), Tree('VP', [Tree('VBP', ["'m"]), + Tree('NP', [Tree('DT', ['a']), Tree('NN', ['dog'])])])])]), Tree('ROOT', [Tree('S', [Tree('NP', + [Tree('DT', ['This'])]), Tree('VP', [Tree('VBZ', ['is']), Tree('NP', [Tree('NP', [Tree('NP', [Tree('PRP$', ['my']), + Tree('NNS', ['friends']), Tree('POS', ["'"])]), Tree('NN', ['cat'])]), Tree('PRN', [Tree('-LRB-', [Tree('', []), + Tree('NP', [Tree('DT', ['the']), Tree('NN', ['tabby'])]), Tree('-RRB-', [])])])])])])])] + + >>> sum([list(dep_graphs) for dep_graphs in parser.tagged_parse_sents(( + ... ( + ... ("The", "DT"), + ... ("quick", "JJ"), + ... ("brown", "JJ"), + ... ("fox", "NN"), + ... ("jumped", "VBD"), + ... ("over", "IN"), + ... ("the", "DT"), + ... ("lazy", "JJ"), + ... ("dog", "NN"), + ... (".", "."), + ... ), + ... ))],[]) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('ROOT', [Tree('S', [Tree('NP', [Tree('DT', ['The']), Tree('JJ', ['quick']), Tree('JJ', ['brown']), + Tree('NN', ['fox'])]), Tree('VP', [Tree('VBD', ['jumped']), Tree('PP', [Tree('IN', ['over']), Tree('NP', + [Tree('DT', ['the']), Tree('JJ', ['lazy']), Tree('NN', ['dog'])])])]), Tree('.', ['.'])])])] + """ + + _OUTPUT_FORMAT = "penn" + + def __init__(self, *args, **kwargs): + warnings.warn( + "The StanfordParser will be deprecated\n" + "Please use \033[91mnltk.parse.corenlp.CoreNLPParser\033[0m instead.", + DeprecationWarning, + stacklevel=2, + ) + + super().__init__(*args, **kwargs) + + def _make_tree(self, result): + return Tree.fromstring(result) + + +class StanfordDependencyParser(GenericStanfordParser): + """ + >>> dep_parser=StanfordDependencyParser( + ... model_path="edu/stanford/nlp/models/lexparser/englishPCFG.ser.gz" + ... ) # doctest: +SKIP + + >>> [parse.tree() for parse in dep_parser.raw_parse("The quick brown fox jumps over the lazy dog.")] # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('jumps', [Tree('fox', ['The', 'quick', 'brown']), Tree('dog', ['over', 'the', 'lazy'])])] + + >>> [list(parse.triples()) for parse in dep_parser.raw_parse("The quick brown fox jumps over the lazy dog.")] # doctest: +NORMALIZE_WHITESPACE +SKIP + [[((u'jumps', u'VBZ'), u'nsubj', (u'fox', u'NN')), ((u'fox', u'NN'), u'det', (u'The', u'DT')), + ((u'fox', u'NN'), u'amod', (u'quick', u'JJ')), ((u'fox', u'NN'), u'amod', (u'brown', u'JJ')), + ((u'jumps', u'VBZ'), u'nmod', (u'dog', u'NN')), ((u'dog', u'NN'), u'case', (u'over', u'IN')), + ((u'dog', u'NN'), u'det', (u'the', u'DT')), ((u'dog', u'NN'), u'amod', (u'lazy', u'JJ'))]] + + >>> sum([[parse.tree() for parse in dep_graphs] for dep_graphs in dep_parser.raw_parse_sents(( + ... "The quick brown fox jumps over the lazy dog.", + ... "The quick grey wolf jumps over the lazy fox." + ... ))], []) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('jumps', [Tree('fox', ['The', 'quick', 'brown']), Tree('dog', ['over', 'the', 'lazy'])]), + Tree('jumps', [Tree('wolf', ['The', 'quick', 'grey']), Tree('fox', ['over', 'the', 'lazy'])])] + + >>> sum([[parse.tree() for parse in dep_graphs] for dep_graphs in dep_parser.parse_sents(( + ... "I 'm a dog".split(), + ... "This is my friends ' cat ( the tabby )".split(), + ... ))], []) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('dog', ['I', "'m", 'a']), Tree('cat', ['This', 'is', Tree('friends', ['my', "'"]), Tree('tabby', ['the'])])] + + >>> sum([[list(parse.triples()) for parse in dep_graphs] for dep_graphs in dep_parser.tagged_parse_sents(( + ... ( + ... ("The", "DT"), + ... ("quick", "JJ"), + ... ("brown", "JJ"), + ... ("fox", "NN"), + ... ("jumped", "VBD"), + ... ("over", "IN"), + ... ("the", "DT"), + ... ("lazy", "JJ"), + ... ("dog", "NN"), + ... (".", "."), + ... ), + ... ))],[]) # doctest: +NORMALIZE_WHITESPACE +SKIP + [[((u'jumped', u'VBD'), u'nsubj', (u'fox', u'NN')), ((u'fox', u'NN'), u'det', (u'The', u'DT')), + ((u'fox', u'NN'), u'amod', (u'quick', u'JJ')), ((u'fox', u'NN'), u'amod', (u'brown', u'JJ')), + ((u'jumped', u'VBD'), u'nmod', (u'dog', u'NN')), ((u'dog', u'NN'), u'case', (u'over', u'IN')), + ((u'dog', u'NN'), u'det', (u'the', u'DT')), ((u'dog', u'NN'), u'amod', (u'lazy', u'JJ'))]] + + """ + + _OUTPUT_FORMAT = "conll2007" + + def __init__(self, *args, **kwargs): + warnings.warn( + "The StanfordDependencyParser will be deprecated\n" + "Please use \033[91mnltk.parse.corenlp.CoreNLPDependencyParser\033[0m instead.", + DeprecationWarning, + stacklevel=2, + ) + + super().__init__(*args, **kwargs) + + def _make_tree(self, result): + return DependencyGraph(result, top_relation_label="root") + + +class StanfordNeuralDependencyParser(GenericStanfordParser): + """ + >>> from nltk.parse.stanford import StanfordNeuralDependencyParser # doctest: +SKIP + >>> dep_parser=StanfordNeuralDependencyParser(java_options='-mx4g')# doctest: +SKIP + + >>> [parse.tree() for parse in dep_parser.raw_parse("The quick brown fox jumps over the lazy dog.")] # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('jumps', [Tree('fox', ['The', 'quick', 'brown']), Tree('dog', ['over', 'the', 'lazy']), '.'])] + + >>> [list(parse.triples()) for parse in dep_parser.raw_parse("The quick brown fox jumps over the lazy dog.")] # doctest: +NORMALIZE_WHITESPACE +SKIP + [[((u'jumps', u'VBZ'), u'nsubj', (u'fox', u'NN')), ((u'fox', u'NN'), u'det', + (u'The', u'DT')), ((u'fox', u'NN'), u'amod', (u'quick', u'JJ')), ((u'fox', u'NN'), + u'amod', (u'brown', u'JJ')), ((u'jumps', u'VBZ'), u'nmod', (u'dog', u'NN')), + ((u'dog', u'NN'), u'case', (u'over', u'IN')), ((u'dog', u'NN'), u'det', + (u'the', u'DT')), ((u'dog', u'NN'), u'amod', (u'lazy', u'JJ')), ((u'jumps', u'VBZ'), + u'punct', (u'.', u'.'))]] + + >>> sum([[parse.tree() for parse in dep_graphs] for dep_graphs in dep_parser.raw_parse_sents(( + ... "The quick brown fox jumps over the lazy dog.", + ... "The quick grey wolf jumps over the lazy fox." + ... ))], []) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('jumps', [Tree('fox', ['The', 'quick', 'brown']), Tree('dog', ['over', + 'the', 'lazy']), '.']), Tree('jumps', [Tree('wolf', ['The', 'quick', 'grey']), + Tree('fox', ['over', 'the', 'lazy']), '.'])] + + >>> sum([[parse.tree() for parse in dep_graphs] for dep_graphs in dep_parser.parse_sents(( + ... "I 'm a dog".split(), + ... "This is my friends ' cat ( the tabby )".split(), + ... ))], []) # doctest: +NORMALIZE_WHITESPACE +SKIP + [Tree('dog', ['I', "'m", 'a']), Tree('cat', ['This', 'is', Tree('friends', + ['my', "'"]), Tree('tabby', ['-LRB-', 'the', '-RRB-'])])] + """ + + _OUTPUT_FORMAT = "conll" + _MAIN_CLASS = "edu.stanford.nlp.pipeline.StanfordCoreNLP" + _JAR = r"stanford-corenlp-(\d+)(\.(\d+))+\.jar" + _MODEL_JAR_PATTERN = r"stanford-corenlp-(\d+)(\.(\d+))+-models\.jar" + _USE_STDIN = True + _DOUBLE_SPACED_OUTPUT = True + + def __init__(self, *args, **kwargs): + warnings.warn( + "The StanfordNeuralDependencyParser will be deprecated\n" + "Please use \033[91mnltk.parse.corenlp.CoreNLPDependencyParser\033[0m instead.", + DeprecationWarning, + stacklevel=2, + ) + + super().__init__(*args, **kwargs) + self.corenlp_options += "-annotators tokenize,ssplit,pos,depparse" + + def tagged_parse_sents(self, sentences, verbose=False): + """ + Currently unimplemented because the neural dependency parser (and + the StanfordCoreNLP pipeline class) doesn't support passing in pre- + tagged tokens. + """ + raise NotImplementedError( + "tagged_parse[_sents] is not supported by " + "StanfordNeuralDependencyParser; use " + "parse[_sents] or raw_parse[_sents] instead." + ) + + def _make_tree(self, result): + return DependencyGraph(result, top_relation_label="ROOT") diff --git a/nltk/parse/test.cfg b/nltk/parse/test.cfg new file mode 100644 index 0000000..5e8e595 --- /dev/null +++ b/nltk/parse/test.cfg @@ -0,0 +1,9 @@ +%start S + +S[sem=] -> NP[sem=?subj] VP[sem=?vp] +VP[sem = ] -> V[sem = ?v] NP[sem=?obj] +VP[sem = ?v] -> V[sem = ?v] +NP[sem = ] -> 'Kim' +NP[sem = ] -> 'I' +V[sem = <\x y.(like x y)>, tns=pres] -> 'like' +V[sem = <\x.(sleeps x)>, tns=pres] -> 'sleeps' diff --git a/nltk/parse/transitionparser.py b/nltk/parse/transitionparser.py new file mode 100644 index 0000000..1799489 --- /dev/null +++ b/nltk/parse/transitionparser.py @@ -0,0 +1,809 @@ +# Natural Language Toolkit: Arc-Standard and Arc-eager Transition Based Parsers +# +# Author: Long Duong +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +import pickle +import tempfile +from copy import deepcopy +from operator import itemgetter +from os import remove + +try: + from numpy import array + from scipy import sparse + from sklearn import svm + from sklearn.datasets import load_svmlight_file +except ImportError: + pass + +from nltk.parse import DependencyEvaluator, DependencyGraph, ParserI +from nltk.picklesec import allowlisted_pickle_load + +# Modules whose globals a saved TransitionParser model legitimately needs. The +# model is a trained scikit-learn classifier backed by numpy/scipy arrays, so +# allowing only these scientific-stack modules lets a genuine model load while +# blocking arbitrary-code gadgets (os, posix, subprocess, builtins, ...). +_MODEL_ALLOWED_MODULES = ("numpy", "scipy", "sklearn") + + +class Configuration: + """ + Class for holding configuration which is the partial analysis of the input sentence. + The transition based parser aims at finding set of operators that transfer the initial + configuration to the terminal configuration. + + The configuration includes: + - Stack: for storing partially proceeded words + - Buffer: for storing remaining input words + - Set of arcs: for storing partially built dependency tree + + This class also provides a method to represent a configuration as list of features. + """ + + def __init__(self, dep_graph): + """ + :param dep_graph: the representation of an input in the form of dependency graph. + :type dep_graph: DependencyGraph where the dependencies are not specified. + """ + # dep_graph.nodes contain list of token for a sentence + self.stack = [0] # The root element + self.buffer = list(range(1, len(dep_graph.nodes))) # The rest is in the buffer + self.arcs = [] # empty set of arc + self._tokens = dep_graph.nodes + self._max_address = len(self.buffer) + + def __str__(self): + return ( + "Stack : " + + str(self.stack) + + " Buffer : " + + str(self.buffer) + + " Arcs : " + + str(self.arcs) + ) + + def _check_informative(self, feat, flag=False): + """ + Check whether a feature is informative + The flag control whether "_" is informative or not + """ + if feat is None: + return False + if feat == "": + return False + if flag is False: + if feat == "_": + return False + return True + + def extract_features(self): + """ + Extract the set of features for the current configuration. Implement standard features as describe in + Table 3.2 (page 31) in Dependency Parsing book by Sandra Kubler, Ryan McDonal, Joakim Nivre. + Please note that these features are very basic. + :return: list(str) + """ + result = [] + # Todo : can come up with more complicated features set for better + # performance. + if len(self.stack) > 0: + # Stack 0 + stack_idx0 = self.stack[len(self.stack) - 1] + token = self._tokens[stack_idx0] + if self._check_informative(token["word"], True): + result.append("STK_0_FORM_" + token["word"]) + if "lemma" in token and self._check_informative(token["lemma"]): + result.append("STK_0_LEMMA_" + token["lemma"]) + if self._check_informative(token["tag"]): + result.append("STK_0_POS_" + token["tag"]) + if "feats" in token and self._check_informative(token["feats"]): + feats = token["feats"].split("|") + for feat in feats: + result.append("STK_0_FEATS_" + feat) + # Stack 1 + if len(self.stack) > 1: + stack_idx1 = self.stack[len(self.stack) - 2] + token = self._tokens[stack_idx1] + if self._check_informative(token["tag"]): + result.append("STK_1_POS_" + token["tag"]) + + # Left most, right most dependency of stack[0] + left_most = 1000000 + right_most = -1 + dep_left_most = "" + dep_right_most = "" + for wi, r, wj in self.arcs: + if wi == stack_idx0: + if (wj > wi) and (wj > right_most): + right_most = wj + dep_right_most = r + if (wj < wi) and (wj < left_most): + left_most = wj + dep_left_most = r + if self._check_informative(dep_left_most): + result.append("STK_0_LDEP_" + dep_left_most) + if self._check_informative(dep_right_most): + result.append("STK_0_RDEP_" + dep_right_most) + + # Check Buffered 0 + if len(self.buffer) > 0: + # Buffer 0 + buffer_idx0 = self.buffer[0] + token = self._tokens[buffer_idx0] + if self._check_informative(token["word"], True): + result.append("BUF_0_FORM_" + token["word"]) + if "lemma" in token and self._check_informative(token["lemma"]): + result.append("BUF_0_LEMMA_" + token["lemma"]) + if self._check_informative(token["tag"]): + result.append("BUF_0_POS_" + token["tag"]) + if "feats" in token and self._check_informative(token["feats"]): + feats = token["feats"].split("|") + for feat in feats: + result.append("BUF_0_FEATS_" + feat) + # Buffer 1 + if len(self.buffer) > 1: + buffer_idx1 = self.buffer[1] + token = self._tokens[buffer_idx1] + if self._check_informative(token["word"], True): + result.append("BUF_1_FORM_" + token["word"]) + if self._check_informative(token["tag"]): + result.append("BUF_1_POS_" + token["tag"]) + if len(self.buffer) > 2: + buffer_idx2 = self.buffer[2] + token = self._tokens[buffer_idx2] + if self._check_informative(token["tag"]): + result.append("BUF_2_POS_" + token["tag"]) + if len(self.buffer) > 3: + buffer_idx3 = self.buffer[3] + token = self._tokens[buffer_idx3] + if self._check_informative(token["tag"]): + result.append("BUF_3_POS_" + token["tag"]) + # Left most, right most dependency of stack[0] + left_most = 1000000 + right_most = -1 + dep_left_most = "" + dep_right_most = "" + for wi, r, wj in self.arcs: + if wi == buffer_idx0: + if (wj > wi) and (wj > right_most): + right_most = wj + dep_right_most = r + if (wj < wi) and (wj < left_most): + left_most = wj + dep_left_most = r + if self._check_informative(dep_left_most): + result.append("BUF_0_LDEP_" + dep_left_most) + if self._check_informative(dep_right_most): + result.append("BUF_0_RDEP_" + dep_right_most) + + return result + + +class Transition: + """ + This class defines a set of transition which is applied to a configuration to get another configuration + Note that for different parsing algorithm, the transition is different. + """ + + # Define set of transitions + LEFT_ARC = "LEFTARC" + RIGHT_ARC = "RIGHTARC" + SHIFT = "SHIFT" + REDUCE = "REDUCE" + + def __init__(self, alg_option): + """ + :param alg_option: the algorithm option of this parser. Currently support `arc-standard` and `arc-eager` algorithm + :type alg_option: str + """ + self._algo = alg_option + if alg_option not in [ + TransitionParser.ARC_STANDARD, + TransitionParser.ARC_EAGER, + ]: + raise ValueError( + " Currently we only support %s and %s " + % (TransitionParser.ARC_STANDARD, TransitionParser.ARC_EAGER) + ) + + def left_arc(self, conf, relation): + """ + Note that the algorithm for left-arc is quite similar except for precondition for both arc-standard and arc-eager + + :param configuration: is the current configuration + :return: A new configuration or -1 if the pre-condition is not satisfied + """ + if (len(conf.buffer) <= 0) or (len(conf.stack) <= 0): + return -1 + if conf.buffer[0] == 0: + # here is the Root element + return -1 + + idx_wi = conf.stack[len(conf.stack) - 1] + + flag = True + if self._algo == TransitionParser.ARC_EAGER: + for idx_parent, r, idx_child in conf.arcs: + if idx_child == idx_wi: + flag = False + + if flag: + conf.stack.pop() + idx_wj = conf.buffer[0] + conf.arcs.append((idx_wj, relation, idx_wi)) + else: + return -1 + + def right_arc(self, conf, relation): + """ + Note that the algorithm for right-arc is DIFFERENT for arc-standard and arc-eager + + :param configuration: is the current configuration + :return: A new configuration or -1 if the pre-condition is not satisfied + """ + if (len(conf.buffer) <= 0) or (len(conf.stack) <= 0): + return -1 + if self._algo == TransitionParser.ARC_STANDARD: + idx_wi = conf.stack.pop() + idx_wj = conf.buffer[0] + conf.buffer[0] = idx_wi + conf.arcs.append((idx_wi, relation, idx_wj)) + else: # arc-eager + idx_wi = conf.stack[len(conf.stack) - 1] + idx_wj = conf.buffer.pop(0) + conf.stack.append(idx_wj) + conf.arcs.append((idx_wi, relation, idx_wj)) + + def reduce(self, conf): + """ + Note that the algorithm for reduce is only available for arc-eager + + :param configuration: is the current configuration + :return: A new configuration or -1 if the pre-condition is not satisfied + """ + + if self._algo != TransitionParser.ARC_EAGER: + return -1 + if len(conf.stack) <= 0: + return -1 + + idx_wi = conf.stack[len(conf.stack) - 1] + flag = False + for idx_parent, r, idx_child in conf.arcs: + if idx_child == idx_wi: + flag = True + if flag: + conf.stack.pop() # reduce it + else: + return -1 + + def shift(self, conf): + """ + Note that the algorithm for shift is the SAME for arc-standard and arc-eager + + :param configuration: is the current configuration + :return: A new configuration or -1 if the pre-condition is not satisfied + """ + if len(conf.buffer) <= 0: + return -1 + idx_wi = conf.buffer.pop(0) + conf.stack.append(idx_wi) + + +class TransitionParser(ParserI): + """ + Class for transition based parser. Implement 2 algorithms which are "arc-standard" and "arc-eager" + """ + + ARC_STANDARD = "arc-standard" + ARC_EAGER = "arc-eager" + + def __init__(self, algorithm): + """ + :param algorithm: the algorithm option of this parser. Currently support `arc-standard` and `arc-eager` algorithm + :type algorithm: str + """ + if algorithm not in [self.ARC_STANDARD, self.ARC_EAGER]: + raise ValueError( + " Currently we only support %s and %s " + % (self.ARC_STANDARD, self.ARC_EAGER) + ) + self._algorithm = algorithm + + self._dictionary = {} + self._transition = {} + self._match_transition = {} + + def _get_dep_relation(self, idx_parent, idx_child, depgraph): + p_node = depgraph.nodes[idx_parent] + c_node = depgraph.nodes[idx_child] + + if c_node["word"] is None: + return None # Root word + + if c_node["head"] == p_node["address"]: + return c_node["rel"] + else: + return None + + def _convert_to_binary_features(self, features): + """ + :param features: list of feature string which is needed to convert to binary features + :type features: list(str) + :return : string of binary features in libsvm format which is 'featureID:value' pairs + """ + unsorted_result = [] + for feature in features: + self._dictionary.setdefault(feature, len(self._dictionary)) + unsorted_result.append(self._dictionary[feature]) + + # Default value of each feature is 1.0 + return " ".join( + str(featureID) + ":1.0" for featureID in sorted(unsorted_result) + ) + + def _is_projective(self, depgraph): + arc_list = [] + for key in depgraph.nodes: + node = depgraph.nodes[key] + + if "head" in node: + childIdx = node["address"] + parentIdx = node["head"] + if parentIdx is not None: + arc_list.append((parentIdx, childIdx)) + + for parentIdx, childIdx in arc_list: + # Ensure that childIdx < parentIdx + if childIdx > parentIdx: + temp = childIdx + childIdx = parentIdx + parentIdx = temp + for k in range(childIdx + 1, parentIdx): + for m in range(len(depgraph.nodes)): + if (m < childIdx) or (m > parentIdx): + if (k, m) in arc_list: + return False + if (m, k) in arc_list: + return False + return True + + def _write_to_file(self, key, binary_features, input_file): + """ + write the binary features to input file and update the transition dictionary + """ + self._transition.setdefault(key, len(self._transition) + 1) + self._match_transition[self._transition[key]] = key + + input_str = str(self._transition[key]) + " " + binary_features + "\n" + input_file.write(input_str.encode("utf-8")) + + def _create_training_examples_arc_std(self, depgraphs, input_file): + """ + Create the training example in the libsvm format and write it to the input_file. + Reference : Page 32, Chapter 3. Dependency Parsing by Sandra Kubler, Ryan McDonal and Joakim Nivre (2009) + """ + operation = Transition(self.ARC_STANDARD) + count_proj = 0 + training_seq = [] + + for depgraph in depgraphs: + if not self._is_projective(depgraph): + continue + + count_proj += 1 + conf = Configuration(depgraph) + while len(conf.buffer) > 0: + b0 = conf.buffer[0] + features = conf.extract_features() + binary_features = self._convert_to_binary_features(features) + + if len(conf.stack) > 0: + s0 = conf.stack[len(conf.stack) - 1] + # Left-arc operation + rel = self._get_dep_relation(b0, s0, depgraph) + if rel is not None: + key = Transition.LEFT_ARC + ":" + rel + self._write_to_file(key, binary_features, input_file) + operation.left_arc(conf, rel) + training_seq.append(key) + continue + + # Right-arc operation + rel = self._get_dep_relation(s0, b0, depgraph) + if rel is not None: + precondition = True + # Get the max-index of buffer + maxID = conf._max_address + + for w in range(maxID + 1): + if w != b0: + relw = self._get_dep_relation(b0, w, depgraph) + if relw is not None: + if (b0, relw, w) not in conf.arcs: + precondition = False + + if precondition: + key = Transition.RIGHT_ARC + ":" + rel + self._write_to_file(key, binary_features, input_file) + operation.right_arc(conf, rel) + training_seq.append(key) + continue + + # Shift operation as the default + key = Transition.SHIFT + self._write_to_file(key, binary_features, input_file) + operation.shift(conf) + training_seq.append(key) + + print(" Number of training examples : " + str(len(depgraphs))) + print(" Number of valid (projective) examples : " + str(count_proj)) + return training_seq + + def _create_training_examples_arc_eager(self, depgraphs, input_file): + """ + Create the training example in the libsvm format and write it to the input_file. + Reference : 'A Dynamic Oracle for Arc-Eager Dependency Parsing' by Joav Goldberg and Joakim Nivre + """ + operation = Transition(self.ARC_EAGER) + countProj = 0 + training_seq = [] + + for depgraph in depgraphs: + if not self._is_projective(depgraph): + continue + + countProj += 1 + conf = Configuration(depgraph) + while len(conf.buffer) > 0: + b0 = conf.buffer[0] + features = conf.extract_features() + binary_features = self._convert_to_binary_features(features) + + if len(conf.stack) > 0: + s0 = conf.stack[len(conf.stack) - 1] + # Left-arc operation + rel = self._get_dep_relation(b0, s0, depgraph) + if rel is not None: + key = Transition.LEFT_ARC + ":" + rel + self._write_to_file(key, binary_features, input_file) + operation.left_arc(conf, rel) + training_seq.append(key) + continue + + # Right-arc operation + rel = self._get_dep_relation(s0, b0, depgraph) + if rel is not None: + key = Transition.RIGHT_ARC + ":" + rel + self._write_to_file(key, binary_features, input_file) + operation.right_arc(conf, rel) + training_seq.append(key) + continue + + # reduce operation + flag = False + for k in range(s0): + if self._get_dep_relation(k, b0, depgraph) is not None: + flag = True + if self._get_dep_relation(b0, k, depgraph) is not None: + flag = True + if flag: + key = Transition.REDUCE + self._write_to_file(key, binary_features, input_file) + operation.reduce(conf) + training_seq.append(key) + continue + + # Shift operation as the default + key = Transition.SHIFT + self._write_to_file(key, binary_features, input_file) + operation.shift(conf) + training_seq.append(key) + + print(" Number of training examples : " + str(len(depgraphs))) + print(" Number of valid (projective) examples : " + str(countProj)) + return training_seq + + def train(self, depgraphs, modelfile, verbose=True): + """ + :param depgraphs : list of DependencyGraph as the training data + :type depgraphs : DependencyGraph + :param modelfile : file name to save the trained model + :type modelfile : str + """ + + try: + input_file = tempfile.NamedTemporaryFile( + prefix="transition_parse.train", dir=tempfile.gettempdir(), delete=False + ) + + if self._algorithm == self.ARC_STANDARD: + self._create_training_examples_arc_std(depgraphs, input_file) + else: + self._create_training_examples_arc_eager(depgraphs, input_file) + + input_file.close() + # Using the temporary file to train the libsvm classifier + x_train, y_train = load_svmlight_file(input_file.name) + x_train = x_train.astype("float64") + x_train.indices = x_train.indices.astype("int32", copy=False) + x_train.indptr = x_train.indptr.astype("int32", copy=False) + # The parameter is set according to the paper: + # Algorithms for Deterministic Incremental Dependency Parsing by Joakim Nivre + # Todo : because of probability = True => very slow due to + # cross-validation. Need to improve the speed here + model = svm.SVC( + kernel="poly", + degree=2, + coef0=0, + gamma=0.2, + C=0.5, + verbose=verbose, + probability=True, + ) + + model.fit(x_train, y_train) + # Save the model to file name (as pickle) + pickle.dump(model, open(modelfile, "wb")) + finally: + remove(input_file.name) + + def parse(self, depgraphs, modelFile): + """ + :param depgraphs: the list of test sentence, each sentence is represented as a dependency graph where the 'head' information is dummy + :type depgraphs: list(DependencyGraph) + :param modelfile: the model file + :type modelfile: str + :return: list (DependencyGraph) with the 'head' and 'rel' information + """ + result = [] + # First load the model. The model is a trained scikit-learn classifier, + # so it is loaded through an allowlisting unpickler (CWE-502): only + # numpy/scipy/sklearn globals may be reconstructed, and anything else -- + # e.g. os.system -- raises UnpicklingError instead of executing. See + # nltk/picklesec.py and huntr report + # https://huntr.com/bounties/38abc191-0525-42a1-96fd-262c1c187012 + with open(modelFile, "rb") as f: + model = allowlisted_pickle_load(f, allowed_modules=_MODEL_ALLOWED_MODULES) + operation = Transition(self._algorithm) + + for depgraph in depgraphs: + conf = Configuration(depgraph) + while len(conf.buffer) > 0: + features = conf.extract_features() + col = [] + row = [] + data = [] + for feature in features: + if feature in self._dictionary: + col.append(self._dictionary[feature]) + row.append(0) + data.append(1.0) + np_col = array(sorted(col)) # NB : index must be sorted + np_row = array(row) + np_data = array(data) + + x_test = sparse.csr_matrix( + (np_data, (np_row, np_col)), shape=(1, len(self._dictionary)) + ) + + # It's best to use decision function as follow BUT it's not supported yet for sparse SVM + # Using decision function to build the votes array + # dec_func = model.decision_function(x_test)[0] + # votes = {} + # k = 0 + # for i in range(len(model.classes_)): + # for j in range(i+1, len(model.classes_)): + # #if dec_func[k] > 0: + # votes.setdefault(i,0) + # votes[i] +=1 + # else: + # votes.setdefault(j,0) + # votes[j] +=1 + # k +=1 + # Sort votes according to the values + # sorted_votes = sorted(votes.items(), key=itemgetter(1), reverse=True) + + # We will use predict_proba instead of decision_function + prob_dict = {} + pred_prob = model.predict_proba(x_test)[0] + for i in range(len(pred_prob)): + prob_dict[i] = pred_prob[i] + sorted_Prob = sorted(prob_dict.items(), key=itemgetter(1), reverse=True) + + # Note that SHIFT is always a valid operation + for y_pred_idx, confidence in sorted_Prob: + # y_pred = model.predict(x_test)[0] + # From the prediction match to the operation + y_pred = model.classes_[y_pred_idx] + + if y_pred in self._match_transition: + strTransition = self._match_transition[y_pred] + baseTransition = strTransition.split(":")[0] + + if baseTransition == Transition.LEFT_ARC: + if ( + operation.left_arc(conf, strTransition.split(":")[1]) + != -1 + ): + break + elif baseTransition == Transition.RIGHT_ARC: + if ( + operation.right_arc(conf, strTransition.split(":")[1]) + != -1 + ): + break + elif baseTransition == Transition.REDUCE: + if operation.reduce(conf) != -1: + break + elif baseTransition == Transition.SHIFT: + if operation.shift(conf) != -1: + break + else: + raise ValueError( + "The predicted transition is not recognized, expected errors" + ) + + # Finish with operations build the dependency graph from Conf.arcs + + new_depgraph = deepcopy(depgraph) + for key in new_depgraph.nodes: + node = new_depgraph.nodes[key] + node["rel"] = "" + # With the default, all the token depend on the Root + node["head"] = 0 + for head, rel, child in conf.arcs: + c_node = new_depgraph.nodes[child] + c_node["head"] = head + c_node["rel"] = rel + result.append(new_depgraph) + + return result + + +def demo(): + """ + >>> from nltk.parse import DependencyGraph, DependencyEvaluator + >>> from nltk.parse.transitionparser import TransitionParser, Configuration, Transition + >>> gold_sent = DependencyGraph(\""" + ... Economic JJ 2 ATT + ... news NN 3 SBJ + ... has VBD 0 ROOT + ... little JJ 5 ATT + ... effect NN 3 OBJ + ... on IN 5 ATT + ... financial JJ 8 ATT + ... markets NNS 6 PC + ... . . 3 PU + ... \""") + + >>> conf = Configuration(gold_sent) + + ###################### Check the Initial Feature ######################## + + >>> print(', '.join(conf.extract_features())) + STK_0_POS_TOP, BUF_0_FORM_Economic, BUF_0_LEMMA_Economic, BUF_0_POS_JJ, BUF_1_FORM_news, BUF_1_POS_NN, BUF_2_POS_VBD, BUF_3_POS_JJ + + ###################### Check The Transition ####################### + Check the Initialized Configuration + >>> print(conf) + Stack : [0] Buffer : [1, 2, 3, 4, 5, 6, 7, 8, 9] Arcs : [] + + A. Do some transition checks for ARC-STANDARD + + >>> operation = Transition('arc-standard') + >>> operation.shift(conf) + >>> operation.left_arc(conf, "ATT") + >>> operation.shift(conf) + >>> operation.left_arc(conf,"SBJ") + >>> operation.shift(conf) + >>> operation.shift(conf) + >>> operation.left_arc(conf, "ATT") + >>> operation.shift(conf) + >>> operation.shift(conf) + >>> operation.shift(conf) + >>> operation.left_arc(conf, "ATT") + + Middle Configuration and Features Check + >>> print(conf) + Stack : [0, 3, 5, 6] Buffer : [8, 9] Arcs : [(2, 'ATT', 1), (3, 'SBJ', 2), (5, 'ATT', 4), (8, 'ATT', 7)] + + >>> print(', '.join(conf.extract_features())) + STK_0_FORM_on, STK_0_LEMMA_on, STK_0_POS_IN, STK_1_POS_NN, BUF_0_FORM_markets, BUF_0_LEMMA_markets, BUF_0_POS_NNS, BUF_1_FORM_., BUF_1_POS_., BUF_0_LDEP_ATT + + >>> operation.right_arc(conf, "PC") + >>> operation.right_arc(conf, "ATT") + >>> operation.right_arc(conf, "OBJ") + >>> operation.shift(conf) + >>> operation.right_arc(conf, "PU") + >>> operation.right_arc(conf, "ROOT") + >>> operation.shift(conf) + + Terminated Configuration Check + >>> print(conf) + Stack : [0] Buffer : [] Arcs : [(2, 'ATT', 1), (3, 'SBJ', 2), (5, 'ATT', 4), (8, 'ATT', 7), (6, 'PC', 8), (5, 'ATT', 6), (3, 'OBJ', 5), (3, 'PU', 9), (0, 'ROOT', 3)] + + + B. Do some transition checks for ARC-EAGER + + >>> conf = Configuration(gold_sent) + >>> operation = Transition('arc-eager') + >>> operation.shift(conf) + >>> operation.left_arc(conf,'ATT') + >>> operation.shift(conf) + >>> operation.left_arc(conf,'SBJ') + >>> operation.right_arc(conf,'ROOT') + >>> operation.shift(conf) + >>> operation.left_arc(conf,'ATT') + >>> operation.right_arc(conf,'OBJ') + >>> operation.right_arc(conf,'ATT') + >>> operation.shift(conf) + >>> operation.left_arc(conf,'ATT') + >>> operation.right_arc(conf,'PC') + >>> operation.reduce(conf) + >>> operation.reduce(conf) + >>> operation.reduce(conf) + >>> operation.right_arc(conf,'PU') + >>> print(conf) + Stack : [0, 3, 9] Buffer : [] Arcs : [(2, 'ATT', 1), (3, 'SBJ', 2), (0, 'ROOT', 3), (5, 'ATT', 4), (3, 'OBJ', 5), (5, 'ATT', 6), (8, 'ATT', 7), (6, 'PC', 8), (3, 'PU', 9)] + + ###################### Check The Training Function ####################### + + A. Check the ARC-STANDARD training + >>> import tempfile + >>> import os + >>> input_file = tempfile.NamedTemporaryFile(prefix='transition_parse.train', dir=tempfile.gettempdir(), delete=False) + + >>> parser_std = TransitionParser('arc-standard') + >>> print(', '.join(parser_std._create_training_examples_arc_std([gold_sent], input_file))) + Number of training examples : 1 + Number of valid (projective) examples : 1 + SHIFT, LEFTARC:ATT, SHIFT, LEFTARC:SBJ, SHIFT, SHIFT, LEFTARC:ATT, SHIFT, SHIFT, SHIFT, LEFTARC:ATT, RIGHTARC:PC, RIGHTARC:ATT, RIGHTARC:OBJ, SHIFT, RIGHTARC:PU, RIGHTARC:ROOT, SHIFT + + >>> parser_std.train([gold_sent],'temp.arcstd.model', verbose=False) + Number of training examples : 1 + Number of valid (projective) examples : 1 + >>> input_file.close() + >>> remove(input_file.name) + + B. Check the ARC-EAGER training + + >>> input_file = tempfile.NamedTemporaryFile(prefix='transition_parse.train', dir=tempfile.gettempdir(),delete=False) + >>> parser_eager = TransitionParser('arc-eager') + >>> print(', '.join(parser_eager._create_training_examples_arc_eager([gold_sent], input_file))) + Number of training examples : 1 + Number of valid (projective) examples : 1 + SHIFT, LEFTARC:ATT, SHIFT, LEFTARC:SBJ, RIGHTARC:ROOT, SHIFT, LEFTARC:ATT, RIGHTARC:OBJ, RIGHTARC:ATT, SHIFT, LEFTARC:ATT, RIGHTARC:PC, REDUCE, REDUCE, REDUCE, RIGHTARC:PU + + >>> parser_eager.train([gold_sent],'temp.arceager.model', verbose=False) + Number of training examples : 1 + Number of valid (projective) examples : 1 + + >>> input_file.close() + >>> remove(input_file.name) + + ###################### Check The Parsing Function ######################## + + A. Check the ARC-STANDARD parser + + >>> result = parser_std.parse([gold_sent], 'temp.arcstd.model') + >>> de = DependencyEvaluator(result, [gold_sent]) + >>> de.eval() >= (0, 0) + True + + B. Check the ARC-EAGER parser + >>> result = parser_eager.parse([gold_sent], 'temp.arceager.model') + >>> de = DependencyEvaluator(result, [gold_sent]) + >>> de.eval() >= (0, 0) + True + + Remove test temporary files + >>> remove('temp.arceager.model') + >>> remove('temp.arcstd.model') + + Note that result is very poor because of only one training example. + """ diff --git a/nltk/parse/util.py b/nltk/parse/util.py new file mode 100644 index 0000000..295ed23 --- /dev/null +++ b/nltk/parse/util.py @@ -0,0 +1,234 @@ +# Natural Language Toolkit: Parser Utility Functions +# +# Author: Ewan Klein +# Tom Aarsen <> +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + + +""" +Utility functions for parsers. +""" + +from nltk.data import load +from nltk.grammar import CFG, PCFG, FeatureGrammar +from nltk.parse.chart import Chart, ChartParser +from nltk.parse.featurechart import FeatureChart, FeatureChartParser +from nltk.parse.pchart import InsideChartParser + + +def load_parser( + grammar_url, trace=0, parser=None, chart_class=None, beam_size=0, **load_args +): + """ + Load a grammar from a file, and build a parser based on that grammar. + The parser depends on the grammar format, and might also depend + on properties of the grammar itself. + + The following grammar formats are currently supported: + - ``'cfg'`` (CFGs: ``CFG``) + - ``'pcfg'`` (probabilistic CFGs: ``PCFG``) + - ``'fcfg'`` (feature-based CFGs: ``FeatureGrammar``) + + :type grammar_url: str + :param grammar_url: A URL specifying where the grammar is located. + The default protocol is ``"nltk:"``, which searches for the file + in the the NLTK data package. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing output. + :param parser: The class used for parsing; should be ``ChartParser`` + or a subclass. + If None, the class depends on the grammar format. + :param chart_class: The class used for storing the chart; + should be ``Chart`` or a subclass. + Only used for CFGs and feature CFGs. + If None, the chart class depends on the grammar format. + :type beam_size: int + :param beam_size: The maximum length for the parser's edge queue. + Only used for probabilistic CFGs. + :param load_args: Keyword parameters used when loading the grammar. + See ``data.load`` for more information. + """ + grammar = load(grammar_url, **load_args) + if not isinstance(grammar, CFG): + raise ValueError("The grammar must be a CFG, " "or a subclass thereof.") + if isinstance(grammar, PCFG): + if parser is None: + parser = InsideChartParser + return parser(grammar, trace=trace, beam_size=beam_size) + + elif isinstance(grammar, FeatureGrammar): + if parser is None: + parser = FeatureChartParser + if chart_class is None: + chart_class = FeatureChart + return parser(grammar, trace=trace, chart_class=chart_class) + + else: # Plain CFG. + if parser is None: + parser = ChartParser + if chart_class is None: + chart_class = Chart + return parser(grammar, trace=trace, chart_class=chart_class) + + +def taggedsent_to_conll(sentence): + """ + A module to convert a single POS tagged sentence into CONLL format. + + >>> from nltk import word_tokenize, pos_tag + >>> text = "This is a foobar sentence." + >>> for line in taggedsent_to_conll(pos_tag(word_tokenize(text))): # doctest: +NORMALIZE_WHITESPACE + ... print(line, end="") + 1 This _ DT DT _ 0 a _ _ + 2 is _ VBZ VBZ _ 0 a _ _ + 3 a _ DT DT _ 0 a _ _ + 4 foobar _ JJ JJ _ 0 a _ _ + 5 sentence _ NN NN _ 0 a _ _ + 6 . _ . . _ 0 a _ _ + + :param sentence: A single input sentence to parse + :type sentence: list(tuple(str, str)) + :rtype: iter(str) + :return: a generator yielding a single sentence in CONLL format. + """ + for i, (word, tag) in enumerate(sentence, start=1): + input_str = [str(i), word, "_", tag, tag, "_", "0", "a", "_", "_"] + input_str = "\t".join(input_str) + "\n" + yield input_str + + +def taggedsents_to_conll(sentences): + """ + A module to convert the a POS tagged document stream + (i.e. list of list of tuples, a list of sentences) and yield lines + in CONLL format. This module yields one line per word and two newlines + for end of sentence. + + >>> from nltk import word_tokenize, sent_tokenize, pos_tag + >>> text = "This is a foobar sentence. Is that right?" + >>> sentences = [pos_tag(word_tokenize(sent)) for sent in sent_tokenize(text)] + >>> for line in taggedsents_to_conll(sentences): # doctest: +NORMALIZE_WHITESPACE + ... if line: + ... print(line, end="") + 1 This _ DT DT _ 0 a _ _ + 2 is _ VBZ VBZ _ 0 a _ _ + 3 a _ DT DT _ 0 a _ _ + 4 foobar _ JJ JJ _ 0 a _ _ + 5 sentence _ NN NN _ 0 a _ _ + 6 . _ . . _ 0 a _ _ + + + 1 Is _ VBZ VBZ _ 0 a _ _ + 2 that _ IN IN _ 0 a _ _ + 3 right _ NN NN _ 0 a _ _ + 4 ? _ . . _ 0 a _ _ + + + + :param sentences: Input sentences to parse + :type sentence: list(list(tuple(str, str))) + :rtype: iter(str) + :return: a generator yielding sentences in CONLL format. + """ + for sentence in sentences: + yield from taggedsent_to_conll(sentence) + yield "\n\n" + + +###################################################################### +# { Test Suites +###################################################################### + + +class TestGrammar: + """ + Unit tests for CFG. + """ + + def __init__(self, grammar, suite, accept=None, reject=None): + self.test_grammar = grammar + + self.cp = load_parser(grammar, trace=0) + self.suite = suite + self._accept = accept + self._reject = reject + + def run(self, show_trees=False): + """ + Sentences in the test suite are divided into two classes: + + - grammatical (``accept``) and + - ungrammatical (``reject``). + + If a sentence should parse according to the grammar, the value of + ``trees`` will be a non-empty list. If a sentence should be rejected + according to the grammar, then the value of ``trees`` will be None. + """ + for test in self.suite: + print(test["doc"] + ":", end=" ") + for key in ["accept", "reject"]: + for sent in test[key]: + tokens = sent.split() + trees = list(self.cp.parse(tokens)) + if show_trees and trees: + print() + print(sent) + for tree in trees: + print(tree) + if key == "accept": + if trees == []: + raise ValueError("Sentence '%s' failed to parse'" % sent) + else: + accepted = True + else: + if trees: + raise ValueError("Sentence '%s' received a parse'" % sent) + else: + rejected = True + if accepted and rejected: + print("All tests passed!") + + +def extract_test_sentences(string, comment_chars="#%;", encoding=None): + """ + Parses a string with one test sentence per line. + Lines can optionally begin with: + + - a bool, saying if the sentence is grammatical or not, or + - an int, giving the number of parse trees is should have, + + The result information is followed by a colon, and then the sentence. + Empty lines and lines beginning with a comment char are ignored. + + :return: a list of tuple of sentences and expected results, + where a sentence is a list of str, + and a result is None, or bool, or int + + :param comment_chars: ``str`` of possible comment characters. + :param encoding: the encoding of the string, if it is binary + """ + if encoding is not None: + string = string.decode(encoding) + sentences = [] + for sentence in string.split("\n"): + if sentence == "" or sentence[0] in comment_chars: + continue + split_info = sentence.split(":", 1) + result = None + if len(split_info) == 2: + if split_info[0] in ["True", "true", "False", "false"]: + result = split_info[0] in ["True", "true"] + sentence = split_info[1] + else: + result = int(split_info[0]) + sentence = split_info[1] + tokens = sentence.split() + if tokens == []: + continue + sentences += [(tokens, result)] + return sentences diff --git a/nltk/parse/viterbi.py b/nltk/parse/viterbi.py new file mode 100644 index 0000000..1f2800f --- /dev/null +++ b/nltk/parse/viterbi.py @@ -0,0 +1,453 @@ +# Natural Language Toolkit: Viterbi Probabilistic Parser +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from functools import reduce + +from nltk.parse.api import ParserI +from nltk.tree import ProbabilisticTree, Tree + +##////////////////////////////////////////////////////// +## Viterbi PCFG Parser +##////////////////////////////////////////////////////// + + +class ViterbiParser(ParserI): + """ + A bottom-up ``PCFG`` parser that uses dynamic programming to find + the single most likely parse for a text. The ``ViterbiParser`` parser + parses texts by filling in a "most likely constituent table". + This table records the most probable tree representation for any + given span and node value. In particular, it has an entry for + every start index, end index, and node value, recording the most + likely subtree that spans from the start index to the end index, + and has the given node value. + + The ``ViterbiParser`` parser fills in this table incrementally. It starts + by filling in all entries for constituents that span one element + of text (i.e., entries where the end index is one greater than the + start index). After it has filled in all table entries for + constituents that span one element of text, it fills in the + entries for constitutants that span two elements of text. It + continues filling in the entries for constituents spanning larger + and larger portions of the text, until the entire table has been + filled. Finally, it returns the table entry for a constituent + spanning the entire text, whose node value is the grammar's start + symbol. + + In order to find the most likely constituent with a given span and + node value, the ``ViterbiParser`` parser considers all productions that + could produce that node value. For each production, it finds all + children that collectively cover the span and have the node values + specified by the production's right hand side. If the probability + of the tree formed by applying the production to the children is + greater than the probability of the current entry in the table, + then the table is updated with this new tree. + + A pseudo-code description of the algorithm used by + ``ViterbiParser`` is: + + | Create an empty most likely constituent table, *MLC*. + | For width in 1...len(text): + | For start in 1...len(text)-width: + | For prod in grammar.productions: + | For each sequence of subtrees [t[1], t[2], ..., t[n]] in MLC, + | where t[i].label()==prod.rhs[i], + | and the sequence covers [start:start+width]: + | old_p = MLC[start, start+width, prod.lhs] + | new_p = P(t[1])P(t[1])...P(t[n])P(prod) + | if new_p > old_p: + | new_tree = Tree(prod.lhs, t[1], t[2], ..., t[n]) + | MLC[start, start+width, prod.lhs] = new_tree + | Return MLC[0, len(text), start_symbol] + + :type _grammar: PCFG + :ivar _grammar: The grammar used to parse sentences. + :type _trace: int + :ivar _trace: The level of tracing output that should be generated + when parsing a text. + """ + + def __init__(self, grammar, trace=0): + """ + Create a new ``ViterbiParser`` parser, that uses ``grammar`` to + parse texts. + + :type grammar: PCFG + :param grammar: The grammar used to parse texts. + :type trace: int + :param trace: The level of tracing that should be used when + parsing a text. ``0`` will generate no tracing output; + and higher numbers will produce more verbose tracing + output. + """ + self._grammar = grammar + self._trace = trace + + def grammar(self): + return self._grammar + + def trace(self, trace=2): + """ + Set the level of tracing output that should be generated when + parsing a text. + + :type trace: int + :param trace: The trace level. A trace level of ``0`` will + generate no tracing output; and higher trace levels will + produce more verbose tracing output. + :rtype: None + """ + self._trace = trace + + def parse(self, tokens): + # Inherit docs from ParserI + + tokens = list(tokens) + self._grammar.check_coverage(tokens) + + # The most likely constituent table. This table specifies the + # most likely constituent for a given span and type. + # Constituents can be either Trees or tokens. For Trees, + # the "type" is the Nonterminal for the tree's root node + # value. For Tokens, the "type" is the token's type. + # The table is stored as a dictionary, since it is sparse. + constituents = {} + + # Initialize the constituents dictionary with the words from + # the text. + if self._trace: + print("Inserting tokens into the most likely" + " constituents table...") + for index in range(len(tokens)): + token = tokens[index] + constituents[index, index + 1, token] = token + if self._trace > 1: + self._trace_lexical_insertion(token, index, len(tokens)) + + # Consider each span of length 1, 2, ..., n; and add any trees + # that might cover that span to the constituents dictionary. + for length in range(1, len(tokens) + 1): + if self._trace: + print( + "Finding the most likely constituents" + + " spanning %d text elements..." % length + ) + for start in range(len(tokens) - length + 1): + span = (start, start + length) + self._add_constituents_spanning(span, constituents, tokens) + + # Return the tree that spans the entire text & have the right cat + tree = constituents.get((0, len(tokens), self._grammar.start())) + if tree is not None: + yield tree + + def _add_constituents_spanning(self, span, constituents, tokens): + """ + Find any constituents that might cover ``span``, and add them + to the most likely constituents table. + + :rtype: None + :type span: tuple(int, int) + :param span: The section of the text for which we are + trying to find possible constituents. The span is + specified as a pair of integers, where the first integer + is the index of the first token that should be included in + the constituent; and the second integer is the index of + the first token that should not be included in the + constituent. I.e., the constituent should cover + ``text[span[0]:span[1]]``, where ``text`` is the text + that we are parsing. + + :type constituents: dict(tuple(int,int,Nonterminal) -> ProbabilisticToken or ProbabilisticTree) + :param constituents: The most likely constituents table. This + table records the most probable tree representation for + any given span and node value. In particular, + ``constituents(s,e,nv)`` is the most likely + ``ProbabilisticTree`` that covers ``text[s:e]`` + and has a node value ``nv.symbol()``, where ``text`` + is the text that we are parsing. When + ``_add_constituents_spanning`` is called, ``constituents`` + should contain all possible constituents that are shorter + than ``span``. + + :type tokens: list of tokens + :param tokens: The text we are parsing. This is only used for + trace output. + """ + # Since some of the grammar productions may be unary, we need to + # repeatedly try all of the productions until none of them add any + # new constituents. + changed = True + while changed: + changed = False + + # Find all ways instantiations of the grammar productions that + # cover the span. + instantiations = self._find_instantiations(span, constituents) + + # For each production instantiation, add a new + # ProbabilisticTree whose probability is the product + # of the childrens' probabilities and the production's + # probability. + for production, children in instantiations: + subtrees = [c for c in children if isinstance(c, Tree)] + p = reduce(lambda pr, t: pr * t.prob(), subtrees, production.prob()) + node = production.lhs().symbol() + tree = ProbabilisticTree(node, children, prob=p) + + # If it's new a constituent, then add it to the + # constituents dictionary. + c = constituents.get((span[0], span[1], production.lhs())) + if self._trace > 1: + if c is None or c != tree: + if c is None or c.prob() < tree.prob(): + print(" Insert:", end=" ") + else: + print(" Discard:", end=" ") + self._trace_production(production, p, span, len(tokens)) + if c is None or c.prob() < tree.prob(): + constituents[span[0], span[1], production.lhs()] = tree + changed = True + + def _find_instantiations(self, span, constituents): + """ + :return: a list of the production instantiations that cover a + given span of the text. A "production instantiation" is + a tuple containing a production and a list of children, + where the production's right hand side matches the list of + children; and the children cover ``span``. :rtype: list + of ``pair`` of ``Production``, (list of + (``ProbabilisticTree`` or token. + + :type span: tuple(int, int) + :param span: The section of the text for which we are + trying to find production instantiations. The span is + specified as a pair of integers, where the first integer + is the index of the first token that should be covered by + the production instantiation; and the second integer is + the index of the first token that should not be covered by + the production instantiation. + :type constituents: dict(tuple(int,int,Nonterminal) -> ProbabilisticToken or ProbabilisticTree) + :param constituents: The most likely constituents table. This + table records the most probable tree representation for + any given span and node value. See the module + documentation for more information. + """ + rv = [] + for production in self._grammar.productions(): + childlists = self._match_rhs(production.rhs(), span, constituents) + + for childlist in childlists: + rv.append((production, childlist)) + return rv + + def _match_rhs(self, rhs, span, constituents): + """ + :return: a set of all the lists of children that cover ``span`` + and that match ``rhs``. + :rtype: list(list(ProbabilisticTree or token) + + :type rhs: list(Nonterminal or any) + :param rhs: The list specifying what kinds of children need to + cover ``span``. Each nonterminal in ``rhs`` specifies + that the corresponding child should be a tree whose node + value is that nonterminal's symbol. Each terminal in ``rhs`` + specifies that the corresponding child should be a token + whose type is that terminal. + :type span: tuple(int, int) + :param span: The section of the text for which we are + trying to find child lists. The span is specified as a + pair of integers, where the first integer is the index of + the first token that should be covered by the child list; + and the second integer is the index of the first token + that should not be covered by the child list. + :type constituents: dict(tuple(int,int,Nonterminal) -> ProbabilisticToken or ProbabilisticTree) + :param constituents: The most likely constituents table. This + table records the most probable tree representation for + any given span and node value. See the module + documentation for more information. + """ + (start, end) = span + + # Base case + if start >= end and rhs == (): + return [[]] + if start >= end or rhs == (): + return [] + + # Find everything that matches the 1st symbol of the RHS + childlists = [] + for split in range(start, end + 1): + l = constituents.get((start, split, rhs[0])) + if l is not None: + rights = self._match_rhs(rhs[1:], (split, end), constituents) + childlists += [[l] + r for r in rights] + + return childlists + + def _trace_production(self, production, p, span, width): + """ + Print trace output indicating that a given production has been + applied at a given location. + + :param production: The production that has been applied + :type production: Production + :param p: The probability of the tree produced by the production. + :type p: float + :param span: The span of the production + :type span: tuple + :rtype: None + """ + + str = "|" + "." * span[0] + str += "=" * (span[1] - span[0]) + str += "." * (width - span[1]) + "| " + str += "%s" % production + if self._trace > 2: + str = f"{str:<40} {p:12.10f} " + + print(str) + + def _trace_lexical_insertion(self, token, index, width): + str = " Insert: |" + "." * index + "=" + "." * (width - index - 1) + "| " + str += f"{token}" + print(str) + + def __repr__(self): + return "" % self._grammar + + +##////////////////////////////////////////////////////// +## Test Code +##////////////////////////////////////////////////////// + + +def demo(): + """ + A demonstration of the probabilistic parsers. The user is + prompted to select which demo to run, and how many parses should + be found; and then each parser is run on the same demo, and a + summary of the results are displayed. + """ + import sys + import time + + from nltk import tokenize + from nltk.grammar import PCFG + from nltk.parse import ViterbiParser + + toy_pcfg1 = PCFG.fromstring( + """ + S -> NP VP [1.0] + NP -> Det N [0.5] | NP PP [0.25] | 'John' [0.1] | 'I' [0.15] + Det -> 'the' [0.8] | 'my' [0.2] + N -> 'man' [0.5] | 'telescope' [0.5] + VP -> VP PP [0.1] | V NP [0.7] | V [0.2] + V -> 'ate' [0.35] | 'saw' [0.65] + PP -> P NP [1.0] + P -> 'with' [0.61] | 'under' [0.39] + """ + ) + + toy_pcfg2 = PCFG.fromstring( + """ + S -> NP VP [1.0] + VP -> V NP [.59] + VP -> V [.40] + VP -> VP PP [.01] + NP -> Det N [.41] + NP -> Name [.28] + NP -> NP PP [.31] + PP -> P NP [1.0] + V -> 'saw' [.21] + V -> 'ate' [.51] + V -> 'ran' [.28] + N -> 'boy' [.11] + N -> 'cookie' [.12] + N -> 'table' [.13] + N -> 'telescope' [.14] + N -> 'hill' [.5] + Name -> 'Jack' [.52] + Name -> 'Bob' [.48] + P -> 'with' [.61] + P -> 'under' [.39] + Det -> 'the' [.41] + Det -> 'a' [.31] + Det -> 'my' [.28] + """ + ) + + # Define two demos. Each demo has a sentence and a grammar. + demos = [ + ("I saw the man with my telescope", toy_pcfg1), + ("the boy saw Jack with Bob under the table with a telescope", toy_pcfg2), + ] + + # Ask the user which demo they want to use. + print() + for i in range(len(demos)): + print(f"{i + 1:>3}: {demos[i][0]}") + print(" %r" % demos[i][1]) + print() + print("Which demo (%d-%d)? " % (1, len(demos)), end=" ") + try: + snum = int(sys.stdin.readline().strip()) - 1 + sent, grammar = demos[snum] + except Exception: + print("Bad sentence number") + return + + # Tokenize the sentence. + tokens = sent.split() + + parser = ViterbiParser(grammar) + all_parses = {} + + print(f"\nsent: {sent}\nparser: {parser}\ngrammar: {grammar}") + parser.trace(3) + t = time.time() + parses = parser.parse_all(tokens) + time = time.time() - t + average = ( + reduce(lambda a, b: a + b.prob(), parses, 0) / len(parses) if parses else 0 + ) + num_parses = len(parses) + for p in parses: + all_parses[p.freeze()] = 1 + + # Print some summary statistics + print() + print("Time (secs) # Parses Average P(parse)") + print("-----------------------------------------") + print("%11.4f%11d%19.14f" % (time, num_parses, average)) + parses = all_parses.keys() + if parses: + p = reduce(lambda a, b: a + b.prob(), parses, 0) / len(parses) + else: + p = 0 + print("------------------------------------------") + print("%11s%11d%19.14f" % ("n/a", len(parses), p)) + + # Ask the user if we should draw the parses. + print() + print("Draw parses (y/n)? ", end=" ") + if sys.stdin.readline().strip().lower().startswith("y"): + from nltk.draw.tree import draw_trees + + print(" please wait...") + draw_trees(*parses) + + # Ask the user if we should print the parses. + print() + print("Print parses (y/n)? ", end=" ") + if sys.stdin.readline().strip().lower().startswith("y"): + for parse in parses: + print(parse) + + +if __name__ == "__main__": + demo() diff --git a/nltk/pathsec.py b/nltk/pathsec.py new file mode 100644 index 0000000..0369958 --- /dev/null +++ b/nltk/pathsec.py @@ -0,0 +1,594 @@ +# Natural Language Toolkit: Centralized I/O security sentinel +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Eric Kafe +# URL: +# For license information, see LICENSE.TXT +# +"""Centralized I/O security sentinel for NLTK.""" + +"""Centralized I/O security sentinel for NLTK.""" +import builtins +import http.client +import ipaddress +import os +import socket +import sys +import urllib.request +import warnings +import zipfile +from functools import lru_cache +from pathlib import Path +from urllib.parse import unquote, urlparse + +# Security Enforcement Toggle +# ENFORCE = False +ENFORCE = True + +_ALLOWED_ROOTS_CACHE = None +_LAST_DATA_PATHS = None + + +def _get_allowed_roots(): + """Dynamically determines allowed directories based on NLTK data paths.""" + global _ALLOWED_ROOTS_CACHE, _LAST_DATA_PATHS + + current_paths = [] + if "nltk.data" in sys.modules: + # Accessing nltk.data.path via sys.modules to avoid top-level circularity + current_paths = list(getattr(sys.modules["nltk.data"], "path", [])) + + env_paths = os.environ.get("NLTK_DATA", "") + current_state = (current_paths, env_paths) + + if _ALLOWED_ROOTS_CACHE is not None and _LAST_DATA_PATHS == current_state: + return _ALLOWED_ROOTS_CACHE + + roots = set() + for p in current_paths + env_paths.split(os.pathsep): + if p: + try: + # Handle both string paths and PathPointer objects + raw_p = p.path if hasattr(p, "path") else p + roots.add(Path(str(raw_p)).resolve()) + except (OSError, ValueError, RuntimeError): + continue + + import tempfile + + for loc in ["~/nltk_data", "/usr/share/nltk_data", tempfile.gettempdir()]: + try: + p = Path(loc).expanduser().resolve() + if p.exists(): + roots.add(p) + except (OSError, ValueError, RuntimeError): + continue + + _ALLOWED_ROOTS_CACHE = roots + _LAST_DATA_PATHS = current_state + return roots + + +def validate_path(path_input, context="NLTK", required_root=None): + """ + Ensures file access is restricted to allowed data directories. + + :param path_input: The path to validate. + :param context: Diagnostic context for warnings/errors. + :param required_root: If provided, enforces that the path is strictly + within this specific directory (scoped sandbox). + """ + if isinstance(path_input, int) or not path_input or not str(path_input).strip(): + return + try: + raw = path_input.path if hasattr(path_input, "path") else str(path_input) + + if "://" in raw: + parsed = urlparse(raw) + if parsed.scheme in ("http", "https", "ftp"): + return + if parsed.scheme == "file": + raw = unquote(parsed.path) + + # Resolve path to catch symlink escapes + try: + target = Path(raw).resolve() + except (OSError, ValueError): + # Fallback for virtual paths inside ZIPs (e.g. corpora/foo.zip/file.txt) + lower_raw = raw.lower() + if ".zip" in lower_raw: + zip_idx = lower_raw.find(".zip") + 4 + target = Path(raw[:zip_idx]).resolve() + else: + target = Path(raw) + + # LAYER 1: Scoped Sandbox (PR #3528 Integration) + # This resolves both target and root to block symlink-based escapes. + if required_root: + root_raw = ( + required_root.path + if hasattr(required_root, "path") + else str(required_root) + ) + scoped_root = Path(root_raw).resolve() + if not (target == scoped_root or target.is_relative_to(scoped_root)): + # Raise ValueError to match NLTK's historical CorpusReader error type + raise ValueError( + f"Security Violation [{context}]: Path {target} escapes root {scoped_root}" + ) + + # LAYER 2: Global NLTK_DATA Sandbox + allowed_roots = _get_allowed_roots() + if any(target == root or target.is_relative_to(root) for root in allowed_roots): + return + + # CWD Fallback (Explicit Opt-In for ENFORCE mode) + try: + cwd = Path(os.getcwd()).resolve() + if target == cwd or target.is_relative_to(cwd): + if any(cwd == root for root in allowed_roots): + return + msg = ( + f"Security Violation [{context}]: CWD access restricted in ENFORCE mode. " + "Authorize via: nltk.data.path.append('.')" + ) + if ENFORCE: + raise PermissionError(msg) + else: + warnings.warn( + f"Security Warning [{context}]: Path {target} allowed via CWD.", + RuntimeWarning, + stacklevel=3, + ) + return + except (OSError, ValueError): + pass + + msg = f"Security Violation [{context}]: Unauthorized path {target}" + if ENFORCE: + raise PermissionError(msg) + else: + warnings.warn(msg, RuntimeWarning, stacklevel=3) + except (PermissionError, ValueError): + raise + except Exception: + if ENFORCE: + raise + + +def _zip_member_is_unsafe(name_str): + """True if a ZIP member is written somewhere other than where it is validated. + + ``zipfile.ZipFile.extract`` sanitises a member name by *dropping* the drive + and every empty / ``.`` / ``..`` component while keeping the rest, whereas + ``Path.resolve`` collapses a ``..`` against its *preceding* component. For a + member such as ``a/../b/x`` the two disagree: it is validated as ``/b/x`` + but written to ``/a/b/x``, which can escape through a pre-existing + symlink at ``/a/b`` that the collapsed validation path never visits. + + The mismatch only ever arises from absolute / drive-qualified / ``..`` members + -- exactly the shapes a legitimate archive never uses -- so rather than keep + two different normalizations in sync we reject them outright. This both + closes the validate/extract gap and is the proactive block the hardened + extractor promises (CWE-22 / CWE-59). + """ + # Normalize every separator zipfile treats as such on this platform to "/". + normalized = name_str.replace("\\", "/") if os.path.altsep else name_str + if os.path.splitdrive(name_str)[0] or normalized.startswith("/"): + return True + return os.path.pardir in normalized.split("/") + + +def validate_zip_archive( + zip_obj_or_path, target_root, specific_member=None, context="ZipAudit" +): + """Enhanced Zip-Slip protection using Pathlib for cross-platform safety.""" + try: + target = Path(target_root).resolve() + + def _audit(zf): + members = ( + [specific_member] if specific_member is not None else zf.namelist() + ) + for name in members: + name_str = name.filename if hasattr(name, "filename") else str(name) + if "\0" in name_str: + raise ValueError(f"Null byte in ZIP member: {name_str}") + + # ``resolve()`` follows symlinks, catching escapes through a + # pre-existing symlinked subpath. The extra component check + # rejects absolute / ``..`` members, whose write target diverges + # from this resolved path (CWE-22 / CWE-59). + member_path = (target / name_str).resolve() + if _zip_member_is_unsafe(name_str) or not ( + member_path == target or member_path.is_relative_to(target) + ): + msg = f"Security Violation [{context}]: Traversal member '{name_str}' detected." + if ENFORCE: + raise PermissionError(msg) + else: + warnings.warn(msg, RuntimeWarning, stacklevel=3) + + if isinstance(zip_obj_or_path, zipfile.ZipFile): + _audit(zip_obj_or_path) + else: + with zipfile.ZipFile(zip_obj_or_path, "r") as zf: + _audit(zf) + except (PermissionError, ValueError): + raise + except (OSError, zipfile.BadZipFile): + if ENFORCE: + raise PermissionError("Zip validation failed") + + +@lru_cache(maxsize=256) +def _resolve_hostname(hostname): + """Cached hostname resolution for the early SSRF pre-check. + + Note: the cache alone does NOT prevent DNS rebinding, because the connection + layer re-resolves the hostname independently. The actual rebinding + protection is the connect-time IP pinning in ``_SafeHTTPConnection`` / + ``_SafeHTTPSConnection``. + """ + try: + return socket.getaddrinfo(hostname, None, proto=socket.IPPROTO_TCP) + except (OSError, ValueError): + return [] + + +# IPv6->IPv4 transition prefixes that have no dedicated stdlib accessor. +_NAT64_WELL_KNOWN = ipaddress.ip_network("64:ff9b::/96") +_IPV4_COMPATIBLE = ipaddress.ip_network("::/96") + + +def _embedded_ipv4(ip): + """The embedded IPv4 for IPv6 forms that *are* an IPv4 address, else None. + + Covers IPv4-mapped (``::ffff:0:0/96``), IPv4-compatible (``::/96``) and the + NAT64 well-known prefix (``64:ff9b::/96``). For these the IPv6 wrapper has no + independent routable meaning, so the embedded IPv4 is what gets reached. + """ + if not isinstance(ip, ipaddress.IPv6Address): + return None + mapped = ip.ipv4_mapped + if mapped is not None: + return mapped + if ip in _NAT64_WELL_KNOWN or ip in _IPV4_COMPATIBLE: + return ipaddress.IPv4Address(ip.packed[-4:]) + return None + + +def _tunneled_ipv4s(ip): + """IPv4 addresses tunneled by routable IPv6 wrappers (6to4 / Teredo).""" + if not isinstance(ip, ipaddress.IPv6Address): + return + sixtofour = ip.sixtofour + if sixtofour is not None: + yield sixtofour + teredo = ip.teredo + if teredo is not None: + yield from teredo # (Teredo server, Teredo client) + + +def _ip_is_forbidden(ip): + """Return True if the SSRF filter must refuse to connect to ``ip``. + + Policy (defense in depth): only *globally routable* addresses are allowed; + anything that is not global -- loopback, link-local, private, carrier-grade + NAT (100.64.0.0/10), reserved, unspecified (``0.0.0.0`` / ``::``), + documentation ranges, etc. -- is forbidden. This generalises the previous + explicit ``loopback / link-local / multicast / private`` list and is a strict + superset of it. Multicast is still rejected explicitly because some CPython + versions classify multicast addresses as ``is_global``. + + IPv6 addresses that embed an IPv4 address are evaluated by that embedded IPv4, + not by the wrapper: the stdlib classifies the wrappers (IPv4-mapped, + IPv4-compatible, NAT64 ``64:ff9b::/96``, 6to4 ``2002::/16``, Teredo + ``2001:0::/32``) as globally routable, so a forbidden internal IPv4 (loopback, + the link-local cloud-metadata address, ...) could otherwise be smuggled past + the check and then routed to that IPv4 by a NAT64/6to4/Teredo gateway + (CWE-918). This extends the previous IPv4-mapped-only unwrap. + """ + embedded = _embedded_ipv4(ip) + if embedded is not None: + ip = embedded + for tunneled in _tunneled_ipv4s(ip): + if tunneled.is_multicast or not tunneled.is_global: + return True + return ip.is_multicast or not ip.is_global + + +def validate_network_url(url_input, context="NetworkIO"): + """Hardened URL validation with SSRF protection.""" + if not url_input or not str(url_input).strip(): + return + try: + parsed = urlparse(str(url_input)) + + if parsed.scheme == "file": + file_path = unquote(parsed.path) + netloc = parsed.netloc + + # Only local file:// URIs are allowed. + # Reject remote/UNC-style authorities so validation matches actual access. + if netloc not in ("", "localhost"): + raise OSError( + f"Security Violation [{context}.file_scheme]: " + f"Non-local file URI authority not allowed: {netloc!r}" + ) + + # Windows file:// URIs arrive like /C:/path/to/file + # Convert them to a native absolute path before validation. + if ( + os.name == "nt" + and len(file_path) >= 3 + and file_path[0] == "/" + and file_path[2] == ":" + ): + file_path = file_path[1:] + + validate_path(file_path, context=f"{context}.file_scheme") + return + + if parsed.scheme not in ("http", "https"): + msg = ( + f"Security Violation [{context}]: Unsupported scheme '{parsed.scheme}'." + ) + if ENFORCE: + raise PermissionError(msg) + else: + warnings.warn(msg, RuntimeWarning, stacklevel=3) + return + + for result in _resolve_hostname(parsed.hostname or ""): + ip = ipaddress.ip_address(result[4][0]) + if _ip_is_forbidden(ip): + msg = f"Security Violation [{context}]: SSRF attempt to restricted IP {ip}" + if ENFORCE: + raise PermissionError(msg) + else: + warnings.warn(msg, RuntimeWarning, stacklevel=3) + except (PermissionError, ValueError): + raise + except Exception: + if ENFORCE: + raise + + +class _ValidatingRedirectHandler(urllib.request.HTTPRedirectHandler): + """Ensures that every step of a redirect chain is re-validated against SSRF.""" + + def redirect_request(self, req, fp, code, msg, headers, newurl): + validate_network_url(newurl, context="NetworkRedirect") + return super().redirect_request(req, fp, code, msg, headers, newurl) + + +def _resolve_and_validate_host(host, port): + """ + Resolve ``host`` once and SSRF-validate *every* address it resolves to. + + Returns the resolved ``getaddrinfo`` records so the caller can connect to a + **pinned** numeric address. Because validation and the subsequent connection + observe the same resolution (the connection is made to the numeric IP, which + triggers no further name lookup), this closes the DNS-rebinding TOCTOU where + a hostname resolves to a public IP during validation and to an internal / + loopback IP during the actual connect. + """ + try: + addrinfo = socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP) + except (OSError, ValueError): + return [] + for res in addrinfo: + try: + ip = ipaddress.ip_address(res[4][0]) + except ValueError: + continue + if _ip_is_forbidden(ip): + msg = f"Security Violation [pathsec.urlopen]: SSRF attempt to restricted IP {ip}" + if ENFORCE: + raise PermissionError(msg) + warnings.warn(msg, RuntimeWarning, stacklevel=2) + return addrinfo + + +def _pinned_connection(host, port, timeout, source_address): + """Open a socket to ``host``/``port`` over an SSRF-validated, pinned address. + + Every address ``host`` resolves to is validated together, then the socket is + opened to those **numeric** addresses, so no second (unvalidated) name lookup + happens at connect time -- this closes the DNS-rebinding TOCTOU. All + validated addresses are tried in order, preserving urllib/socket's normal + dual-stack / multi-A fallback. If nothing resolves to a validated address we + fail closed: we never fall back to connecting by the raw hostname, which + would re-resolve unvalidated and reopen the rebinding hole. + """ + addrinfo = _resolve_and_validate_host(host, port) + if not addrinfo: + # Fail closed: never fall back to connecting by the raw hostname (that + # would re-resolve unvalidated and reopen the rebinding hole). The host + # produced no usable address, which is a name-resolution failure, so we + # surface it as socket.gaierror rather than a bare OSError. gaierror is + # an OSError subclass, so urllib still wraps it as URLError and the + # fail-closed contract is unchanged; but callers that legitimately + # expect a DNS failure -- e.g. obfuscated/decimal-IP hosts that some + # platforms (Windows) refuse to resolve -- then see the expected + # gaierror reason instead of an opaque OSError. + raise socket.gaierror( + f"pathsec.urlopen: no validated address for host {host!r}; " + "refusing to connect by unvalidated hostname" + ) + last_err = None + for res in addrinfo: + ip = res[4][0] + try: + return socket.create_connection((ip, port), timeout, source_address) + except OSError as e: + last_err = e + raise last_err + + +class _SafeHTTPConnection(http.client.HTTPConnection): + """HTTPConnection that resolves, SSRF-validates and pins the address at connect().""" + + def connect(self): + self.sock = _pinned_connection( + self.host, self.port, self.timeout, self.source_address + ) + + +class _SafeHTTPSConnection(http.client.HTTPSConnection): + """HTTPS variant of :class:`_SafeHTTPConnection`. + + Connects to a validated, pinned IP but keeps SNI / certificate verification + against the original hostname. + """ + + def connect(self): + sock = _pinned_connection( + self.host, self.port, self.timeout, self.source_address + ) + self.sock = self._context.wrap_socket(sock, server_hostname=self.host) + + +class _SafeHTTPHandler(urllib.request.HTTPHandler): + def http_open(self, req): + return self.do_open(_SafeHTTPConnection, req) + + +class _SafeHTTPSHandler(urllib.request.HTTPSHandler): + def https_open(self, req): + kwargs = {} + if getattr(self, "_context", None) is not None: + kwargs["context"] = self._context + if getattr(self, "_check_hostname", None) is not None: + kwargs["check_hostname"] = self._check_hostname + return self.do_open(_SafeHTTPSConnection, req, **kwargs) + + +def urlopen(url, *args, **kwargs): + """ + Secure wrapper for urllib.request.urlopen with redirect validation. + Inherits NLTK proxy settings, but intentionally ignores other custom + global handlers to strictly enforce the security sandbox. + """ + url_str = url.full_url if hasattr(url, "full_url") else str(url) + validate_network_url(url_str, context="pathsec.urlopen") + + # Start with our security-enforcing redirect handler + handlers = [_ValidatingRedirectHandler()] + + # Safely inherit proxy settings without reusing handler instances + # (Reusing instances overwrites their .parent, breaking the global opener) + proxied = False + has_proxy_handler = False + if urllib.request._opener is not None: + for handler in urllib.request._opener.handlers: + if isinstance(handler, urllib.request.ProxyHandler): + has_proxy_handler = True + # Copy the dictionary to prevent shared mutable state + isolated_proxies = dict(handler.proxies) if handler.proxies else {} + if isolated_proxies: + proxied = True + handlers.append(urllib.request.ProxyHandler(isolated_proxies)) + elif isinstance(handler, urllib.request.ProxyBasicAuthHandler): + handlers.append(urllib.request.ProxyBasicAuthHandler(handler.passwd)) + elif isinstance(handler, urllib.request.ProxyDigestAuthHandler): + handlers.append(urllib.request.ProxyDigestAuthHandler(handler.passwd)) + + # If the caller configured no ProxyHandler at all, environment proxies still + # apply: build_opener() would install a default ProxyHandler from + # getproxies(). Treat that as proxied too, because the proxy -- not NLTK -- + # is then the egress that resolves names and performs the CONNECT tunnel; the + # connect-time pinning handlers cannot tunnel and would break proxied HTTPS. + if not proxied and not has_proxy_handler and urllib.request.getproxies(): + proxied = True + + if not proxied: + # No proxy in effect: NLTK makes the connection itself, so pin the + # validated IP (a rebinding hostname cannot be re-resolved to an internal + # address at connect time). Add an explicit empty ProxyHandler so + # build_opener() does not silently re-enable environment proxies, which + # the pinning handlers cannot tunnel through. + if not has_proxy_handler: + handlers.append(urllib.request.ProxyHandler({})) + handlers.append(_SafeHTTPHandler()) + handlers.append(_SafeHTTPSHandler()) + + opener = urllib.request.build_opener(*handlers) + return opener.open(url, *args, **kwargs) + + +def open(file, mode="r", *, context="pathsec.open", required_root=None, **kwargs): + """Secure wrapper for builtins.open.""" + # 1. Allow file descriptors (integers) to pass through, matching original logic + if isinstance(file, int): + validate_path(file, context=context, required_root=required_root) + return builtins.open(file, mode=mode, **kwargs) + + # 2. Force extraction of the real path from PathLike objects + try: + raw_path = os.fspath(file) + except TypeError: + raise TypeError("Path must be a string, bytes, or os.PathLike") from None + + # 3. Strict primitive enforcement against type manipulation + if type(raw_path) not in (str, bytes): + raise TypeError( + f"Strict security policy: Path must resolve to exact str or bytes, not '{type(raw_path).__name__}'" + ) + + if type(raw_path) is bytes: + raw_path = os.fsdecode(raw_path) + + # 4. Execution Substitution: validate and open the pure primitive, discarding the original object + validate_path(raw_path, context=context, required_root=required_root) + return builtins.open(raw_path, mode=mode, **kwargs) + + +class ZipFile(zipfile.ZipFile): + """Secure wrapper for zipfile.ZipFile.""" + + def __init__(self, file, *args, **kwargs): + # zipfile.ZipFile also accepts file-like objects (e.g., io.BytesIO). + # We only strictly normalize and validate path-like objects. + if isinstance(file, (str, bytes, os.PathLike)): + try: + raw_path = os.fspath(file) + except TypeError: + raise TypeError( + "Path must be a string, bytes, or os.PathLike" + ) from None + + if type(raw_path) not in (str, bytes): + raise TypeError( + f"Strict security policy: Path must resolve to exact str or bytes, not '{type(raw_path).__name__}'" + ) + + if type(raw_path) is bytes: + raw_path = os.fsdecode(raw_path) + + validate_path(raw_path, context="pathsec.ZipFile") + file_to_open = raw_path + else: + file_to_open = file + + super().__init__(file_to_open, *args, **kwargs) + + def extract(self, member, path=None, pwd=None): + validate_zip_archive(self, path or os.getcwd(), specific_member=member) + return super().extract(member, path, pwd) + + def extractall(self, path=None, members=None, pwd=None): + validate_zip_archive(self, path or os.getcwd()) + super().extractall(path, members, pwd) + + +__all__ = [ + "validate_path", + "validate_network_url", + "validate_zip_archive", + "open", + "urlopen", + "ZipFile", + "ENFORCE", +] diff --git a/nltk/picklesec.py b/nltk/picklesec.py new file mode 100644 index 0000000..c4fa2c6 --- /dev/null +++ b/nltk/picklesec.py @@ -0,0 +1,143 @@ +# Natural Language Toolkit: Safer pickle loading. +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Eric Kafe +# URL: +# For license information, see LICENSE.TXT +# + +""" +Helpers for safer and/or more explicit pickle usage in NLTK. + +- RestrictedUnpickler: blocks unpickling of *any* globals (classes/functions). + Intended for loading NLTK data packages where we control the serialization. +- WarningUnpickler: emits a security warning before unpickling (does not make + unpickling safe). +- AllowlistUnpickler: only reconstructs an explicit, audited allowlist of + globals. Use it for loading objects whose set of legitimate classes is known + (e.g. a trained model) so that arbitrary-code gadgets such as ``os.system`` + can never be reconstructed from an untrusted file. +""" + +from __future__ import annotations + +import pickle +import warnings +from collections.abc import Iterable +from typing import Any, BinaryIO + +PICKLE_WARNING = ( + "Security warning: loading pickles can execute arbitrary code. " + "Only load pickle files from trusted sources and never from untrusted " + "or unauthenticated locations." +) + + +class RestrictedUnpickler(pickle.Unpickler): + """ + Unpickler that prevents any class or function from being used during loading. + """ + + def find_class(self, module: str, name: str) -> Any: + # Forbid every function/class global. + raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") + + +class WarningUnpickler(pickle.Unpickler): + """Unpickler that emits PICKLE_WARNING once per instance.""" + + def __init__(self, file: BinaryIO, *, context: str | None = None, **kwargs: Any): + super().__init__(file, **kwargs) + self._context = context + self._warned = False + + def load(self) -> Any: + if not self._warned: + msg = ( + PICKLE_WARNING + if self._context is None + else f"{PICKLE_WARNING} ({self._context})" + ) + warnings.warn(msg, RuntimeWarning, stacklevel=3) + self._warned = True + return super().load() + + +def pickle_load( + file: BinaryIO, *, context: str | None = None, restricted: bool = False +) -> Any: + """ + Convenience wrapper similar to pickle.load(file). + + - If restricted=True, uses RestrictedUnpickler (no warning by default). + - If restricted=False, uses WarningUnpickler and emits a warning. + """ + if restricted: + return RestrictedUnpickler(file).load() + return WarningUnpickler(file, context=context).load() + + +class AllowlistUnpickler(pickle.Unpickler): + """ + Unpickler that only reconstructs an explicit, audited allowlist of globals. + + Two complementary allowlists are supported: + + - ``allowed_globals``: a set of exact ``(module, qualname)`` pairs. + - ``allowed_modules``: a set of module names; a global is permitted when its + module equals an allowed name or is a submodule of one (i.e. ``module`` + equals ``name`` or starts with ``name + "."``). + + Anything not covered by either allowlist raises ``UnpicklingError`` before + the global is resolved, so dangerous callables such as ``os.system`` or + ``builtins.eval`` can never be reconstructed from an untrusted pickle. This + is stricter than :func:`pickle_load` (which only warns and then executes) + but, unlike :class:`RestrictedUnpickler` (which blocks *all* globals), it + still allows the known-good classes a saved object legitimately needs. + """ + + def __init__( + self, + file: BinaryIO, + *, + allowed_globals: Iterable[tuple[str, str]] = (), + allowed_modules: Iterable[str] = (), + **kwargs: Any, + ): + super().__init__(file, **kwargs) + if isinstance(allowed_modules, str): + allowed_modules = (allowed_modules,) + self._allowed_globals = set(allowed_globals) + self._allowed_modules = tuple(allowed_modules) + + def _module_allowed(self, module: str) -> bool: + return any( + module == name or module.startswith(name + ".") + for name in self._allowed_modules + ) + + def find_class(self, module: str, name: str) -> Any: + if (module, name) in self._allowed_globals or self._module_allowed(module): + return super().find_class(module, name) + raise pickle.UnpicklingError( + f"global '{module}.{name}' is not in the pickle allowlist" + ) + + +def allowlisted_pickle_load( + file: BinaryIO, + *, + allowed_globals: Iterable[tuple[str, str]] = (), + allowed_modules: Iterable[str] = (), +) -> Any: + """ + Load a pickle while only permitting an explicit allowlist of globals. + + See :class:`AllowlistUnpickler` for the meaning of ``allowed_globals`` and + ``allowed_modules``. Prefer this over the warn-only :func:`pickle_load` when + the set of legitimate classes in the file is known (e.g. a trained model), + because :func:`pickle_load` still executes arbitrary code. + """ + return AllowlistUnpickler( + file, allowed_globals=allowed_globals, allowed_modules=allowed_modules + ).load() diff --git a/nltk/probability.py b/nltk/probability.py new file mode 100755 index 0000000..6cfec14 --- /dev/null +++ b/nltk/probability.py @@ -0,0 +1,2578 @@ +# Natural Language Toolkit: Probability and Statistics +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (additions) +# Trevor Cohn (additions) +# Peter Ljunglöf (additions) +# Liang Dong (additions) +# Geoffrey Sampson (additions) +# Ilia Kurenkov (additions) +# +# URL: +# For license information, see LICENSE.TXT + +""" +Classes for representing and processing probabilistic information. + +The ``FreqDist`` class is used to encode "frequency distributions", +which count the number of times that each outcome of an experiment +occurs. + +The ``ProbDistI`` class defines a standard interface for "probability +distributions", which encode the probability of each outcome for an +experiment. There are two types of probability distribution: + + - "derived probability distributions" are created from frequency + distributions. They attempt to model the probability distribution + that generated the frequency distribution. + - "analytic probability distributions" are created directly from + parameters (such as variance). + +The ``ConditionalFreqDist`` class and ``ConditionalProbDistI`` interface +are used to encode conditional distributions. Conditional probability +distributions can be derived or analytic; but currently the only +implementation of the ``ConditionalProbDistI`` interface is +``ConditionalProbDist``, a derived distribution. + +""" + +import array +import math +import random +import warnings +from abc import ABCMeta, abstractmethod +from collections import Counter, defaultdict +from functools import reduce + +from nltk.internals import raise_unorderable_types + +_NINF = float("-1e300") + +##////////////////////////////////////////////////////// +## Frequency Distributions +##////////////////////////////////////////////////////// + + +class FreqDist(Counter): + """ + A frequency distribution for the outcomes of an experiment. A + frequency distribution records the number of times each outcome of + an experiment has occurred. For example, a frequency distribution + could be used to record the frequency of each word type in a + document. Formally, a frequency distribution can be defined as a + function mapping from each sample to the number of times that + sample occurred as an outcome. + + Frequency distributions are generally constructed by running a + number of experiments, and incrementing the count for a sample + every time it is an outcome of an experiment. For example, the + following code will produce a frequency distribution that encodes + how often each word occurs in a text: + + >>> from nltk.tokenize import word_tokenize + >>> from nltk.probability import FreqDist + >>> sent = 'This is an example sentence' + >>> fdist = FreqDist() + >>> for word in word_tokenize(sent): + ... fdist[word.lower()] += 1 + + An equivalent way to do this is with the initializer: + + >>> fdist = FreqDist(word.lower() for word in word_tokenize(sent)) + + """ + + def __init__(self, samples=None): + """ + Construct a new frequency distribution. If ``samples`` is + given, then the frequency distribution will be initialized + with the count of each object in ``samples``; otherwise, it + will be initialized to be empty. + + In particular, ``FreqDist()`` returns an empty frequency + distribution; and ``FreqDist(samples)`` first creates an empty + frequency distribution, and then calls ``update`` with the + list ``samples``. + + :param samples: The samples to initialize the frequency + distribution with. + :type samples: Sequence + """ + Counter.__init__(self, samples) + + # Cached number of samples in this FreqDist + self._N = None + + def N(self): + """ + Return the total number of sample outcomes that have been + recorded by this FreqDist. For the number of unique + sample values (or bins) with counts greater than zero, use + ``FreqDist.B()``. + + :rtype: int + """ + if self._N is None: + # Not already cached, or cache has been invalidated + self._N = sum(self.values()) + return self._N + + def __setitem__(self, key, val): + """ + Override ``Counter.__setitem__()`` to invalidate the cached N + """ + self._N = None + super().__setitem__(key, val) + + def __delitem__(self, key): + """ + Override ``Counter.__delitem__()`` to invalidate the cached N + """ + self._N = None + super().__delitem__(key) + + def update(self, *args, **kwargs): + """ + Override ``Counter.update()`` to invalidate the cached N + """ + self._N = None + super().update(*args, **kwargs) + + def setdefault(self, key, val): + """ + Override ``Counter.setdefault()`` to invalidate the cached N + """ + self._N = None + super().setdefault(key, val) + + def B(self): + """ + Return the total number of sample values (or "bins") that + have counts greater than zero. For the total + number of sample outcomes recorded, use ``FreqDist.N()``. + (FreqDist.B() is the same as len(FreqDist).) + + :rtype: int + """ + return len(self) + + def hapaxes(self): + """ + Return a list of all samples that occur once (hapax legomena) + + :rtype: list + """ + return [item for item in self if self[item] == 1] + + def Nr(self, r, bins=None): + return self.r_Nr(bins)[r] + + def r_Nr(self, bins=None): + """ + Return the dictionary mapping r to Nr, the number of samples with frequency r, where Nr > 0. + + :type bins: int + :param bins: The number of possible sample outcomes. ``bins`` + is used to calculate Nr(0). In particular, Nr(0) is + ``bins-self.B()``. If ``bins`` is not specified, it + defaults to ``self.B()`` (so Nr(0) will be 0). + :rtype: int + """ + + _r_Nr = defaultdict(int) + for count in self.values(): + _r_Nr[count] += 1 + + # Special case for Nr[0]: + _r_Nr[0] = bins - self.B() if bins is not None else 0 + + return _r_Nr + + def _cumulative_frequencies(self, samples): + """ + Return the cumulative frequencies of the specified samples. + If no samples are specified, all counts are returned, starting + with the largest. + + :param samples: the samples whose frequencies should be returned. + :type samples: any + :rtype: list(float) + """ + cf = 0.0 + for sample in samples: + cf += self[sample] + yield cf + + # slightly odd nomenclature freq() if FreqDist does counts and ProbDist does probs, + # here, freq() does probs + def freq(self, sample): + """ + Return the frequency of a given sample. The frequency of a + sample is defined as the count of that sample divided by the + total number of sample outcomes that have been recorded by + this FreqDist. The count of a sample is defined as the + number of times that sample outcome was recorded by this + FreqDist. Frequencies are always real numbers in the range + [0, 1]. + + :param sample: the sample whose frequency + should be returned. + :type sample: any + :rtype: float + """ + n = self.N() + if n == 0: + return 0 + return self[sample] / n + + def max(self): + """ + Return the sample with the greatest number of outcomes in this + frequency distribution. If two or more samples have the same + number of outcomes, return one of them; which sample is + returned is undefined. If no outcomes have occurred in this + frequency distribution, return None. + + :return: The sample with the maximum number of outcomes in this + frequency distribution. + :rtype: any or None + """ + if len(self) == 0: + raise ValueError( + "A FreqDist must have at least one sample before max is defined." + ) + return self.most_common(1)[0][0] + + def plot( + self, *args, title="", cumulative=False, percents=False, show=False, **kwargs + ): + """ + Plot samples from the frequency distribution + displaying the most frequent sample first. If an integer + parameter is supplied, stop after this many samples have been + plotted. For a cumulative plot, specify cumulative=True. Additional + ``**kwargs`` are passed to matplotlib's plot function. + (Requires Matplotlib to be installed.) + + :param title: The title for the graph. + :type title: str + :param cumulative: Whether the plot is cumulative. (default = False) + :type cumulative: bool + :param percents: Whether the plot uses percents instead of counts. (default = False) + :type percents: bool + :param show: Whether to show the plot, or only return the ax. + :type show: bool + """ + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ValueError( + "The plot function requires matplotlib to be installed." + "See https://matplotlib.org/" + ) from e + + if len(args) == 0: + args = [len(self)] + samples = [item for item, _ in self.most_common(*args)] + + if cumulative: + freqs = list(self._cumulative_frequencies(samples)) + ylabel = "Cumulative " + else: + freqs = [self[sample] for sample in samples] + ylabel = "" + + if percents: + freqs = [f / self.N() * 100 for f in freqs] + ylabel += "Percents" + else: + ylabel += "Counts" + + ax = plt.gca() + ax.grid(True, color="silver") + + if "linewidth" not in kwargs: + kwargs["linewidth"] = 2 + if title: + ax.set_title(title) + + ax.plot(freqs, **kwargs) + ax.set_xticks(range(len(samples))) + ax.set_xticklabels([str(s) for s in samples], rotation=90) + ax.set_xlabel("Samples") + ax.set_ylabel(ylabel) + + if show: + plt.show() + + return ax + + def tabulate(self, *args, **kwargs): + """ + Tabulate the given samples from the frequency distribution (cumulative), + displaying the most frequent sample first. If an integer + parameter is supplied, stop after this many samples have been + plotted. + + :param samples: The samples to plot (default is all samples) + :type samples: list + :param cumulative: A flag to specify whether the freqs are cumulative (default = False) + :type title: bool + """ + if len(args) == 0: + args = [len(self)] + samples = _get_kwarg( + kwargs, "samples", [item for item, _ in self.most_common(*args)] + ) + + cumulative = _get_kwarg(kwargs, "cumulative", False) + if cumulative: + freqs = list(self._cumulative_frequencies(samples)) + else: + freqs = [self[sample] for sample in samples] + # percents = [f * 100 for f in freqs] only in ProbDist? + + width = max(len(f"{s}") for s in samples) + width = max(width, max(len("%d" % f) for f in freqs)) + + for i in range(len(samples)): + print("%*s" % (width, samples[i]), end=" ") + print() + for i in range(len(samples)): + print("%*d" % (width, freqs[i]), end=" ") + print() + + def copy(self): + """ + Create a copy of this frequency distribution. + + :rtype: FreqDist + """ + return self.__class__(self) + + # Mathematical operatiors + + def __add__(self, other): + """ + Add counts from two counters. + + >>> FreqDist('abbb') + FreqDist('bcc') + FreqDist({'b': 4, 'c': 2, 'a': 1}) + + """ + return self.__class__(super().__add__(other)) + + def __sub__(self, other): + """ + Subtract count, but keep only results with positive counts. + + >>> FreqDist('abbbc') - FreqDist('bccd') + FreqDist({'b': 2, 'a': 1}) + + """ + return self.__class__(super().__sub__(other)) + + def __or__(self, other): + """ + Union is the maximum of value in either of the input counters. + + >>> FreqDist('abbb') | FreqDist('bcc') + FreqDist({'b': 3, 'c': 2, 'a': 1}) + + """ + return self.__class__(super().__or__(other)) + + def __and__(self, other): + """ + Intersection is the minimum of corresponding counts. + + >>> FreqDist('abbb') & FreqDist('bcc') + FreqDist({'b': 1}) + + """ + return self.__class__(super().__and__(other)) + + def __le__(self, other): + """ + Returns True if this frequency distribution is a subset of the other + and for no key the value exceeds the value of the same key from + the other frequency distribution. + + The <= operator forms partial order and satisfying the axioms + reflexivity, antisymmetry and transitivity. + + >>> FreqDist('a') <= FreqDist('a') + True + >>> a = FreqDist('abc') + >>> b = FreqDist('aabc') + >>> (a <= b, b <= a) + (True, False) + >>> FreqDist('a') <= FreqDist('abcd') + True + >>> FreqDist('abc') <= FreqDist('xyz') + False + >>> FreqDist('xyz') <= FreqDist('abc') + False + >>> c = FreqDist('a') + >>> d = FreqDist('aa') + >>> e = FreqDist('aaa') + >>> c <= d and d <= e and c <= e + True + """ + if not isinstance(other, FreqDist): + raise_unorderable_types("<=", self, other) + return set(self).issubset(other) and all( + self[key] <= other[key] for key in self + ) + + def __ge__(self, other): + if not isinstance(other, FreqDist): + raise_unorderable_types(">=", self, other) + return set(self).issuperset(other) and all( + self[key] >= other[key] for key in other + ) + + __lt__ = lambda self, other: self <= other and not self == other + __gt__ = lambda self, other: self >= other and not self == other + + def __repr__(self): + """ + Return a string representation of this FreqDist. + + :rtype: string + """ + return self.pformat() + + def pprint(self, maxlen=10, stream=None): + """ + Print a string representation of this FreqDist to 'stream' + + :param maxlen: The maximum number of items to print + :type maxlen: int + :param stream: The stream to print to. stdout by default + """ + print(self.pformat(maxlen=maxlen), file=stream) + + def pformat(self, maxlen=10): + """ + Return a string representation of this FreqDist. + + :param maxlen: The maximum number of items to display + :type maxlen: int + :rtype: string + """ + items = ["{!r}: {!r}".format(*item) for item in self.most_common(maxlen)] + if len(self) > maxlen: + items.append("...") + return "FreqDist({{{0}}})".format(", ".join(items)) + + def __str__(self): + """ + Return a string representation of this FreqDist. + + :rtype: string + """ + return "" % (len(self), self.N()) + + def __iter__(self): + """ + Return an iterator which yields tokens ordered by frequency. + + :rtype: iterator + """ + for token, _ in self.most_common(self.B()): + yield token + + +##////////////////////////////////////////////////////// +## Probability Distributions +##////////////////////////////////////////////////////// + + +class ProbDistI(metaclass=ABCMeta): + """ + A probability distribution for the outcomes of an experiment. A + probability distribution specifies how likely it is that an + experiment will have any given outcome. For example, a + probability distribution could be used to predict the probability + that a token in a document will have a given type. Formally, a + probability distribution can be defined as a function mapping from + samples to nonnegative real numbers, such that the sum of every + number in the function's range is 1.0. A ``ProbDist`` is often + used to model the probability distribution of the experiment used + to generate a frequency distribution. + """ + + SUM_TO_ONE = True + """True if the probabilities of the samples in this probability + distribution will always sum to one.""" + + @abstractmethod + def __init__(self): + """ + Classes inheriting from ProbDistI should implement __init__. + """ + + @abstractmethod + def prob(self, sample): + """ + Return the probability for a given sample. Probabilities + are always real numbers in the range [0, 1]. + + :param sample: The sample whose probability + should be returned. + :type sample: any + :rtype: float + """ + + def logprob(self, sample): + """ + Return the base 2 logarithm of the probability for a given sample. + + :param sample: The sample whose probability + should be returned. + :type sample: any + :rtype: float + """ + # Default definition, in terms of prob() + p = self.prob(sample) + return math.log(p, 2) if p != 0 else _NINF + + @abstractmethod + def max(self): + """ + Return the sample with the greatest probability. If two or + more samples have the same probability, return one of them; + which sample is returned is undefined. + + :rtype: any + """ + + @abstractmethod + def samples(self): + """ + Return a list of all samples that have nonzero probabilities. + Use ``prob`` to find the probability of each sample. + + :rtype: list + """ + + # cf self.SUM_TO_ONE + def discount(self): + """ + Return the ratio by which counts are discounted on average: c*/c + + :rtype: float + """ + return 0.0 + + # Subclasses should define more efficient implementations of this, + # where possible. + def generate(self): + """ + Return a randomly selected sample from this probability distribution. + The probability of returning each sample ``samp`` is equal to + ``self.prob(samp)``. + """ + p = random.random() + p_init = p + for sample in self.samples(): + p -= self.prob(sample) + if p <= 0: + return sample + # allow for some rounding error: + if p < 0.0001: + return sample + # we *should* never get here + if self.SUM_TO_ONE: + warnings.warn( + "Probability distribution %r sums to %r; generate()" + " is returning an arbitrary sample." % (self, p_init - p) + ) + return random.choice(list(self.samples())) + + +class UniformProbDist(ProbDistI): + """ + A probability distribution that assigns equal probability to each + sample in a given set; and a zero probability to all other + samples. + """ + + def __init__(self, samples): + """ + Construct a new uniform probability distribution, that assigns + equal probability to each sample in ``samples``. + + :param samples: The samples that should be given uniform + probability. + :type samples: list + :raise ValueError: If ``samples`` is empty. + """ + if len(samples) == 0: + raise ValueError( + "A Uniform probability distribution must " + "have at least one sample." + ) + self._sampleset = set(samples) + self._prob = 1.0 / len(self._sampleset) + self._samples = list(self._sampleset) + + def prob(self, sample): + return self._prob if sample in self._sampleset else 0 + + def max(self): + return self._samples[0] + + def samples(self): + return self._samples + + def __repr__(self): + return "" % len(self._sampleset) + + +class RandomProbDist(ProbDistI): + """ + Generates a random probability distribution whereby each sample + will be between 0 and 1 with equal probability (uniform random distribution. + Also called a continuous uniform distribution). + """ + + def __init__(self, samples): + if len(samples) == 0: + raise ValueError( + "A probability distribution must " + "have at least one sample." + ) + self._probs = self.unirand(samples) + self._samples = list(self._probs.keys()) + + @classmethod + def unirand(cls, samples): + """ + The key function that creates a randomized initial distribution + that still sums to 1. Set as a dictionary of prob values so that + it can still be passed to MutableProbDist and called with identical + syntax to UniformProbDist + """ + samples = set(samples) + randrow = [random.random() for i in range(len(samples))] + total = sum(randrow) + for i, x in enumerate(randrow): + randrow[i] = x / total + + total = sum(randrow) + if total != 1: + # this difference, if present, is so small (near NINF) that it + # can be subtracted from any element without risking probs not (0 1) + randrow[-1] -= total - 1 + + return {s: randrow[i] for i, s in enumerate(samples)} + + def max(self): + if not hasattr(self, "_max"): + self._max = max((p, v) for (v, p) in self._probs.items())[1] + return self._max + + def prob(self, sample): + return self._probs.get(sample, 0) + + def samples(self): + return self._samples + + def __repr__(self): + return "" % len(self._probs) + + +class DictionaryProbDist(ProbDistI): + """ + A probability distribution whose probabilities are directly + specified by a given dictionary. The given dictionary maps + samples to probabilities. + """ + + def __init__(self, prob_dict=None, log=False, normalize=False): + """ + Construct a new probability distribution from the given + dictionary, which maps values to probabilities (or to log + probabilities, if ``log`` is true). If ``normalize`` is + true, then the probability values are scaled by a constant + factor such that they sum to 1. + + If called without arguments, the resulting probability + distribution assigns zero probability to all values. + """ + + self._prob_dict = prob_dict.copy() if prob_dict is not None else {} + self._log = log + + # Normalize the distribution, if requested. + if normalize: + if len(prob_dict) == 0: + raise ValueError( + "A DictionaryProbDist must have at least one sample " + + "before it can be normalized." + ) + if log: + value_sum = sum_logs(list(self._prob_dict.values())) + if value_sum <= _NINF: + logp = math.log(1.0 / len(prob_dict), 2) + for x in prob_dict: + self._prob_dict[x] = logp + else: + for x, p in self._prob_dict.items(): + self._prob_dict[x] -= value_sum + else: + value_sum = sum(self._prob_dict.values()) + if value_sum == 0: + p = 1.0 / len(prob_dict) + for x in prob_dict: + self._prob_dict[x] = p + else: + norm_factor = 1.0 / value_sum + for x, p in self._prob_dict.items(): + self._prob_dict[x] *= norm_factor + + def prob(self, sample): + if self._log: + return 2 ** (self._prob_dict[sample]) if sample in self._prob_dict else 0 + else: + return self._prob_dict.get(sample, 0) + + def logprob(self, sample): + if self._log: + return self._prob_dict.get(sample, _NINF) + else: + if sample not in self._prob_dict: + return _NINF + elif self._prob_dict[sample] == 0: + return _NINF + else: + return math.log(self._prob_dict[sample], 2) + + def max(self): + if not hasattr(self, "_max"): + self._max = max((p, v) for (v, p) in self._prob_dict.items())[1] + return self._max + + def samples(self): + return self._prob_dict.keys() + + def __repr__(self): + return "" % len(self._prob_dict) + + +class MLEProbDist(ProbDistI): + """ + The maximum likelihood estimate for the probability distribution + of the experiment used to generate a frequency distribution. The + "maximum likelihood estimate" approximates the probability of + each sample as the frequency of that sample in the frequency + distribution. + """ + + def __init__(self, freqdist, bins=None): + """ + Use the maximum likelihood estimate to create a probability + distribution for the experiment used to generate ``freqdist``. + + :type freqdist: FreqDist + :param freqdist: The frequency distribution that the + probability estimates should be based on. + """ + self._freqdist = freqdist + + def freqdist(self): + """ + Return the frequency distribution that this probability + distribution is based on. + + :rtype: FreqDist + """ + return self._freqdist + + def prob(self, sample): + return self._freqdist.freq(sample) + + def max(self): + return self._freqdist.max() + + def samples(self): + return self._freqdist.keys() + + def __repr__(self): + """ + :rtype: str + :return: A string representation of this ``ProbDist``. + """ + return "" % self._freqdist.N() + + +class LidstoneProbDist(ProbDistI): + """ + The Lidstone estimate for the probability distribution of the + experiment used to generate a frequency distribution. The + "Lidstone estimate" is parameterized by a real number *gamma*, + which typically ranges from 0 to 1. The Lidstone estimate + approximates the probability of a sample with count *c* from an + experiment with *N* outcomes and *B* bins as + ``c+gamma)/(N+B*gamma)``. This is equivalent to adding + *gamma* to the count for each bin, and taking the maximum + likelihood estimate of the resulting frequency distribution. + """ + + SUM_TO_ONE = False + + def __init__(self, freqdist, gamma, bins=None): + """ + Use the Lidstone estimate to create a probability distribution + for the experiment used to generate ``freqdist``. + + :type freqdist: FreqDist + :param freqdist: The frequency distribution that the + probability estimates should be based on. + :type gamma: float + :param gamma: A real number used to parameterize the + estimate. The Lidstone estimate is equivalent to adding + *gamma* to the count for each bin, and taking the + maximum likelihood estimate of the resulting frequency + distribution. + :type bins: int + :param bins: The number of sample values that can be generated + by the experiment that is described by the probability + distribution. This value must be correctly set for the + probabilities of the sample values to sum to one. If + ``bins`` is not specified, it defaults to ``freqdist.B()``. + """ + if (bins == 0) or (bins is None and freqdist.N() == 0): + name = self.__class__.__name__[:-8] + raise ValueError( + "A %s probability distribution " % name + "must have at least one bin." + ) + if (bins is not None) and (bins < freqdist.B()): + name = self.__class__.__name__[:-8] + raise ValueError( + "\nThe number of bins in a %s distribution " % name + + "(%d) must be greater than or equal to\n" % bins + + "the number of bins in the FreqDist used " + + "to create it (%d)." % freqdist.B() + ) + + self._freqdist = freqdist + self._gamma = float(gamma) + self._N = self._freqdist.N() + + if bins is None: + bins = freqdist.B() + self._bins = bins + + self._divisor = self._N + bins * gamma + if self._divisor == 0.0: + # In extreme cases we force the probability to be 0, + # which it will be, since the count will be 0: + self._gamma = 0 + self._divisor = 1 + + def freqdist(self): + """ + Return the frequency distribution that this probability + distribution is based on. + + :rtype: FreqDist + """ + return self._freqdist + + def prob(self, sample): + c = self._freqdist[sample] + return (c + self._gamma) / self._divisor + + def max(self): + # For Lidstone distributions, probability is monotonic with + # frequency, so the most probable sample is the one that + # occurs most frequently. + return self._freqdist.max() + + def samples(self): + return self._freqdist.keys() + + def discount(self): + gb = self._gamma * self._bins + return gb / (self._N + gb) + + def __repr__(self): + """ + Return a string representation of this ``ProbDist``. + + :rtype: str + """ + return "" % self._freqdist.N() + + +class LaplaceProbDist(LidstoneProbDist): + """ + The Laplace estimate for the probability distribution of the + experiment used to generate a frequency distribution. The + "Laplace estimate" approximates the probability of a sample with + count *c* from an experiment with *N* outcomes and *B* bins as + *(c+1)/(N+B)*. This is equivalent to adding one to the count for + each bin, and taking the maximum likelihood estimate of the + resulting frequency distribution. + """ + + def __init__(self, freqdist, bins=None): + """ + Use the Laplace estimate to create a probability distribution + for the experiment used to generate ``freqdist``. + + :type freqdist: FreqDist + :param freqdist: The frequency distribution that the + probability estimates should be based on. + :type bins: int + :param bins: The number of sample values that can be generated + by the experiment that is described by the probability + distribution. This value must be correctly set for the + probabilities of the sample values to sum to one. If + ``bins`` is not specified, it defaults to ``freqdist.B()``. + """ + LidstoneProbDist.__init__(self, freqdist, 1, bins) + + def __repr__(self): + """ + :rtype: str + :return: A string representation of this ``ProbDist``. + """ + return "" % self._freqdist.N() + + +class ELEProbDist(LidstoneProbDist): + """ + The expected likelihood estimate for the probability distribution + of the experiment used to generate a frequency distribution. The + "expected likelihood estimate" approximates the probability of a + sample with count *c* from an experiment with *N* outcomes and + *B* bins as *(c+0.5)/(N+B/2)*. This is equivalent to adding 0.5 + to the count for each bin, and taking the maximum likelihood + estimate of the resulting frequency distribution. + """ + + def __init__(self, freqdist, bins=None): + """ + Use the expected likelihood estimate to create a probability + distribution for the experiment used to generate ``freqdist``. + + :type freqdist: FreqDist + :param freqdist: The frequency distribution that the + probability estimates should be based on. + :type bins: int + :param bins: The number of sample values that can be generated + by the experiment that is described by the probability + distribution. This value must be correctly set for the + probabilities of the sample values to sum to one. If + ``bins`` is not specified, it defaults to ``freqdist.B()``. + """ + LidstoneProbDist.__init__(self, freqdist, 0.5, bins) + + def __repr__(self): + """ + Return a string representation of this ``ProbDist``. + + :rtype: str + """ + return "" % self._freqdist.N() + + +class HeldoutProbDist(ProbDistI): + """ + The heldout estimate for the probability distribution of the + experiment used to generate two frequency distributions. These + two frequency distributions are called the "heldout frequency + distribution" and the "base frequency distribution." The + "heldout estimate" uses uses the "heldout frequency + distribution" to predict the probability of each sample, given its + frequency in the "base frequency distribution". + + In particular, the heldout estimate approximates the probability + for a sample that occurs *r* times in the base distribution as + the average frequency in the heldout distribution of all samples + that occur *r* times in the base distribution. + + This average frequency is *Tr[r]/(Nr[r].N)*, where: + + - *Tr[r]* is the total count in the heldout distribution for + all samples that occur *r* times in the base distribution. + - *Nr[r]* is the number of samples that occur *r* times in + the base distribution. + - *N* is the number of outcomes recorded by the heldout + frequency distribution. + + In order to increase the efficiency of the ``prob`` member + function, *Tr[r]/(Nr[r].N)* is precomputed for each value of *r* + when the ``HeldoutProbDist`` is created. + + :type _estimate: list(float) + :ivar _estimate: A list mapping from *r*, the number of + times that a sample occurs in the base distribution, to the + probability estimate for that sample. ``_estimate[r]`` is + calculated by finding the average frequency in the heldout + distribution of all samples that occur *r* times in the base + distribution. In particular, ``_estimate[r]`` = + *Tr[r]/(Nr[r].N)*. + :type _max_r: int + :ivar _max_r: The maximum number of times that any sample occurs + in the base distribution. ``_max_r`` is used to decide how + large ``_estimate`` must be. + """ + + SUM_TO_ONE = False + + def __init__(self, base_fdist, heldout_fdist, bins=None): + """ + Use the heldout estimate to create a probability distribution + for the experiment used to generate ``base_fdist`` and + ``heldout_fdist``. + + :type base_fdist: FreqDist + :param base_fdist: The base frequency distribution. + :type heldout_fdist: FreqDist + :param heldout_fdist: The heldout frequency distribution. + :type bins: int + :param bins: The number of sample values that can be generated + by the experiment that is described by the probability + distribution. This value must be correctly set for the + probabilities of the sample values to sum to one. If + ``bins`` is not specified, it defaults to ``freqdist.B()``. + """ + + self._base_fdist = base_fdist + self._heldout_fdist = heldout_fdist + + # The max number of times any sample occurs in base_fdist. + self._max_r = base_fdist[base_fdist.max()] + + # Calculate Tr, Nr, and N. + Tr = self._calculate_Tr() + r_Nr = base_fdist.r_Nr(bins) + Nr = [r_Nr[r] for r in range(self._max_r + 1)] + N = heldout_fdist.N() + + # Use Tr, Nr, and N to compute the probability estimate for + # each value of r. + self._estimate = self._calculate_estimate(Tr, Nr, N) + + def _calculate_Tr(self): + """ + Return the list *Tr*, where *Tr[r]* is the total count in + ``heldout_fdist`` for all samples that occur *r* + times in ``base_fdist``. + + :rtype: list(float) + """ + Tr = [0.0] * (self._max_r + 1) + for sample in self._heldout_fdist: + r = self._base_fdist[sample] + Tr[r] += self._heldout_fdist[sample] + return Tr + + def _calculate_estimate(self, Tr, Nr, N): + """ + Return the list *estimate*, where *estimate[r]* is the probability + estimate for any sample that occurs *r* times in the base frequency + distribution. In particular, *estimate[r]* is *Tr[r]/(N[r].N)*. + In the special case that *N[r]=0*, *estimate[r]* will never be used; + so we define *estimate[r]=None* for those cases. + + :rtype: list(float) + :type Tr: list(float) + :param Tr: the list *Tr*, where *Tr[r]* is the total count in + the heldout distribution for all samples that occur *r* + times in base distribution. + :type Nr: list(float) + :param Nr: The list *Nr*, where *Nr[r]* is the number of + samples that occur *r* times in the base distribution. + :type N: int + :param N: The total number of outcomes recorded by the heldout + frequency distribution. + """ + estimate = [] + for r in range(self._max_r + 1): + if Nr[r] == 0: + estimate.append(None) + else: + estimate.append(Tr[r] / (Nr[r] * N)) + return estimate + + def base_fdist(self): + """ + Return the base frequency distribution that this probability + distribution is based on. + + :rtype: FreqDist + """ + return self._base_fdist + + def heldout_fdist(self): + """ + Return the heldout frequency distribution that this + probability distribution is based on. + + :rtype: FreqDist + """ + return self._heldout_fdist + + def samples(self): + return self._base_fdist.keys() + + def prob(self, sample): + # Use our precomputed probability estimate. + r = self._base_fdist[sample] + return self._estimate[r] + + def max(self): + # Note: the Heldout estimation is *not* necessarily monotonic; + # so this implementation is currently broken. However, it + # should give the right answer *most* of the time. :) + return self._base_fdist.max() + + def discount(self): + raise NotImplementedError() + + def __repr__(self): + """ + :rtype: str + :return: A string representation of this ``ProbDist``. + """ + s = "" + return s % (self._base_fdist.N(), self._heldout_fdist.N()) + + +class CrossValidationProbDist(ProbDistI): + """ + The cross-validation estimate for the probability distribution of + the experiment used to generate a set of frequency distribution. + The "cross-validation estimate" for the probability of a sample + is found by averaging the held-out estimates for the sample in + each pair of frequency distributions. + """ + + SUM_TO_ONE = False + + def __init__(self, freqdists, bins): + """ + Use the cross-validation estimate to create a probability + distribution for the experiment used to generate + ``freqdists``. + + :type freqdists: list(FreqDist) + :param freqdists: A list of the frequency distributions + generated by the experiment. + :type bins: int + :param bins: The number of sample values that can be generated + by the experiment that is described by the probability + distribution. This value must be correctly set for the + probabilities of the sample values to sum to one. If + ``bins`` is not specified, it defaults to ``freqdist.B()``. + """ + self._freqdists = freqdists + + # Create a heldout probability distribution for each pair of + # frequency distributions in freqdists. + self._heldout_probdists = [] + for fdist1 in freqdists: + for fdist2 in freqdists: + if fdist1 is not fdist2: + probdist = HeldoutProbDist(fdist1, fdist2, bins) + self._heldout_probdists.append(probdist) + + def freqdists(self): + """ + Return the list of frequency distributions that this ``ProbDist`` is based on. + + :rtype: list(FreqDist) + """ + return self._freqdists + + def samples(self): + # [xx] nb: this is not too efficient + return set(sum((list(fd) for fd in self._freqdists), [])) + + def prob(self, sample): + # Find the average probability estimate returned by each + # heldout distribution. + prob = 0.0 + for heldout_probdist in self._heldout_probdists: + prob += heldout_probdist.prob(sample) + return prob / len(self._heldout_probdists) + + def discount(self): + raise NotImplementedError() + + def __repr__(self): + """ + Return a string representation of this ``ProbDist``. + + :rtype: str + """ + return "" % len(self._freqdists) + + +class WittenBellProbDist(ProbDistI): + """ + The Witten-Bell estimate of a probability distribution. This distribution + allocates uniform probability mass to as yet unseen events by using the + number of events that have only been seen once. The probability mass + reserved for unseen events is equal to *T / (N + T)* + where *T* is the number of observed event types and *N* is the total + number of observed events. This equates to the maximum likelihood estimate + of a new type event occurring. The remaining probability mass is discounted + such that all probability estimates sum to one, yielding: + + - *p = T / Z (N + T)*, if count = 0 + - *p = c / (N + T)*, otherwise + """ + + def __init__(self, freqdist, bins=None): + """ + Creates a distribution of Witten-Bell probability estimates. This + distribution allocates uniform probability mass to as yet unseen + events by using the number of events that have only been seen once. The + probability mass reserved for unseen events is equal to *T / (N + T)* + where *T* is the number of observed event types and *N* is the total + number of observed events. This equates to the maximum likelihood + estimate of a new type event occurring. The remaining probability mass + is discounted such that all probability estimates sum to one, + yielding: + + - *p = T / Z (N + T)*, if count = 0 + - *p = c / (N + T)*, otherwise + + The parameters *T* and *N* are taken from the ``freqdist`` parameter + (the ``B()`` and ``N()`` values). The normalizing factor *Z* is + calculated using these values along with the ``bins`` parameter. + + :param freqdist: The frequency counts upon which to base the + estimation. + :type freqdist: FreqDist + :param bins: The number of possible event types. This must be at least + as large as the number of bins in the ``freqdist``. If None, then + it's assumed to be equal to that of the ``freqdist`` + :type bins: int + """ + assert bins is None or bins >= freqdist.B(), ( + "bins parameter must not be less than %d=freqdist.B()" % freqdist.B() + ) + if bins is None: + bins = freqdist.B() + self._freqdist = freqdist + self._T = self._freqdist.B() + self._Z = bins - self._freqdist.B() + self._N = self._freqdist.N() + # self._P0 is P(0), precalculated for efficiency: + if self._N == 0: + # if freqdist is empty, we approximate P(0) by a UniformProbDist: + self._P0 = 1.0 / self._Z + else: + self._P0 = self._T / (self._Z * (self._N + self._T)) + + def prob(self, sample): + # inherit docs from ProbDistI + c = self._freqdist[sample] + return c / (self._N + self._T) if c != 0 else self._P0 + + def max(self): + return self._freqdist.max() + + def samples(self): + return self._freqdist.keys() + + def freqdist(self): + return self._freqdist + + def discount(self): + raise NotImplementedError() + + def __repr__(self): + """ + Return a string representation of this ``ProbDist``. + + :rtype: str + """ + return "" % self._freqdist.N() + + +##////////////////////////////////////////////////////// +## Good-Turing Probability Distributions +##////////////////////////////////////////////////////// + +# Good-Turing frequency estimation was contributed by Alan Turing and +# his statistical assistant I.J. Good, during their collaboration in +# the WWII. It is a statistical technique for predicting the +# probability of occurrence of objects belonging to an unknown number +# of species, given past observations of such objects and their +# species. (In drawing balls from an urn, the 'objects' would be balls +# and the 'species' would be the distinct colors of the balls (finite +# but unknown in number). +# +# Good-Turing method calculates the probability mass to assign to +# events with zero or low counts based on the number of events with +# higher counts. It does so by using the adjusted count *c\**: +# +# - *c\* = (c + 1) N(c + 1) / N(c)* for c >= 1 +# - *things with frequency zero in training* = N(1) for c == 0 +# +# where *c* is the original count, *N(i)* is the number of event types +# observed with count *i*. We can think the count of unseen as the count +# of frequency one (see Jurafsky & Martin 2nd Edition, p101). +# +# This method is problematic because the situation ``N(c+1) == 0`` +# is quite common in the original Good-Turing estimation; smoothing or +# interpolation of *N(i)* values is essential in practice. +# +# Bill Gale and Geoffrey Sampson present a simple and effective approach, +# Simple Good-Turing. As a smoothing curve they simply use a power curve: +# +# Nr = a*r^b (with b < -1 to give the appropriate hyperbolic +# relationship) +# +# They estimate a and b by simple linear regression technique on the +# logarithmic form of the equation: +# +# log Nr = a + b*log(r) +# +# However, they suggest that such a simple curve is probably only +# appropriate for high values of r. For low values of r, they use the +# measured Nr directly. (see M&S, p.213) +# +# Gale and Sampson propose to use r while the difference between r and +# r* is 1.96 greater than the standard deviation, and switch to r* if +# it is less or equal: +# +# |r - r*| > 1.96 * sqrt((r + 1)^2 (Nr+1 / Nr^2) (1 + Nr+1 / Nr)) +# +# The 1.96 coefficient correspond to a 0.05 significance criterion, +# some implementations can use a coefficient of 1.65 for a 0.1 +# significance criterion. +# + +##////////////////////////////////////////////////////// +## Simple Good-Turing Probablity Distributions +##////////////////////////////////////////////////////// + + +class SimpleGoodTuringProbDist(ProbDistI): + """ + SimpleGoodTuring ProbDist approximates from frequency to frequency of + frequency into a linear line under log space by linear regression. + Details of Simple Good-Turing algorithm can be found in: + + - Good Turing smoothing without tears" (Gale & Sampson 1995), + Journal of Quantitative Linguistics, vol. 2 pp. 217-237. + - "Speech and Language Processing (Jurafsky & Martin), + 2nd Edition, Chapter 4.5 p103 (log(Nc) = a + b*log(c)) + - https://www.grsampson.net/RGoodTur.html + + Given a set of pair (xi, yi), where the xi denotes the frequency and + yi denotes the frequency of frequency, we want to minimize their + square variation. E(x) and E(y) represent the mean of xi and yi. + + - slope: b = sigma ((xi-E(x)(yi-E(y))) / sigma ((xi-E(x))(xi-E(x))) + - intercept: a = E(y) - b.E(x) + """ + + SUM_TO_ONE = False + + def __init__(self, freqdist, bins=None): + """ + :param freqdist: The frequency counts upon which to base the + estimation. + :type freqdist: FreqDist + :param bins: The number of possible event types. This must be + larger than the number of bins in the ``freqdist``. If None, + then it's assumed to be equal to ``freqdist``.B() + 1 + :type bins: int + """ + assert ( + bins is None or bins > freqdist.B() + ), "bins parameter must not be less than %d=freqdist.B()+1" % (freqdist.B() + 1) + if bins is None: + bins = freqdist.B() + 1 + self._freqdist = freqdist + self._bins = bins + r, nr = self._r_Nr() + self.find_best_fit(r, nr) + self._switch(r, nr) + self._renormalize(r, nr) + + def _r_Nr_non_zero(self): + r_Nr = self._freqdist.r_Nr() + del r_Nr[0] + return r_Nr + + def _r_Nr(self): + """ + Split the frequency distribution in two list (r, Nr), where Nr(r) > 0 + """ + nonzero = self._r_Nr_non_zero() + + if not nonzero: + return [], [] + return zip(*sorted(nonzero.items())) + + def find_best_fit(self, r, nr): + """ + Use simple linear regression to tune parameters self._slope and + self._intercept in the log-log space based on count and Nr(count) + (Work in log space to avoid floating point underflow.) + """ + # For higher sample frequencies the data points becomes horizontal + # along line Nr=1. To create a more evident linear model in log-log + # space, we average positive Nr values with the surrounding zero + # values. (Church and Gale, 1991) + + if not r or not nr: + # Empty r or nr? + return + + zr = [] + for j in range(len(r)): + i = r[j - 1] if j > 0 else 0 + k = 2 * r[j] - i if j == len(r) - 1 else r[j + 1] + zr_ = 2.0 * nr[j] / (k - i) + zr.append(zr_) + + log_r = [math.log(i) for i in r] + log_zr = [math.log(i) for i in zr] + + xy_cov = x_var = 0.0 + x_mean = sum(log_r) / len(log_r) + y_mean = sum(log_zr) / len(log_zr) + for x, y in zip(log_r, log_zr): + xy_cov += (x - x_mean) * (y - y_mean) + x_var += (x - x_mean) ** 2 + self._slope = xy_cov / x_var if x_var != 0 else 0.0 + if self._slope >= -1: + warnings.warn( + "SimpleGoodTuring did not find a proper best fit " + "line for smoothing probabilities of occurrences. " + "The probability estimates are likely to be " + "unreliable." + ) + self._intercept = y_mean - self._slope * x_mean + + def _switch(self, r, nr): + """ + Calculate the r frontier where we must switch from Nr to Sr + when estimating E[Nr]. + """ + for i, r_ in enumerate(r): + if len(r) == i + 1 or r[i + 1] != r_ + 1: + # We are at the end of r, or there is a gap in r + self._switch_at = r_ + break + + Sr = self.smoothedNr + smooth_r_star = (r_ + 1) * Sr(r_ + 1) / Sr(r_) + unsmooth_r_star = (r_ + 1) * nr[i + 1] / nr[i] + + std = math.sqrt(self._variance(r_, nr[i], nr[i + 1])) + if abs(unsmooth_r_star - smooth_r_star) <= 1.96 * std: + self._switch_at = r_ + break + + def _variance(self, r, nr, nr_1): + r = float(r) + nr = float(nr) + nr_1 = float(nr_1) + return (r + 1.0) ** 2 * (nr_1 / nr**2) * (1.0 + nr_1 / nr) + + def _renormalize(self, r, nr): + """ + It is necessary to renormalize all the probability estimates to + ensure a proper probability distribution results. This can be done + by keeping the estimate of the probability mass for unseen items as + N(1)/N and renormalizing all the estimates for previously seen items + (as Gale and Sampson (1995) propose). (See M&S P.213, 1999) + """ + prob_cov = 0.0 + for r_, nr_ in zip(r, nr): + prob_cov += nr_ * self._prob_measure(r_) + if prob_cov: + self._renormal = (1 - self._prob_measure(0)) / prob_cov + + def smoothedNr(self, r): + """ + Return the number of samples with count r. + + :param r: The amount of frequency. + :type r: int + :rtype: float + """ + + # Nr = a*r^b (with b < -1 to give the appropriate hyperbolic + # relationship) + # Estimate a and b by simple linear regression technique on + # the logarithmic form of the equation: log Nr = a + b*log(r) + + return math.exp(self._intercept + self._slope * math.log(r)) + + def prob(self, sample): + """ + Return the sample's probability. + + :param sample: sample of the event + :type sample: str + :rtype: float + """ + count = self._freqdist[sample] + p = self._prob_measure(count) + if count == 0: + if self._bins == self._freqdist.B(): + p = 0.0 + else: + p = p / (self._bins - self._freqdist.B()) + else: + p = p * self._renormal + return p + + def _prob_measure(self, count): + if count == 0 and self._freqdist.N() == 0: + return 1.0 + elif count == 0 and self._freqdist.N() != 0: + return self._freqdist.Nr(1) / self._freqdist.N() + + if self._switch_at > count: + Er_1 = self._freqdist.Nr(count + 1) + Er = self._freqdist.Nr(count) + else: + Er_1 = self.smoothedNr(count + 1) + Er = self.smoothedNr(count) + + r_star = (count + 1) * Er_1 / Er + return r_star / self._freqdist.N() + + def check(self): + prob_sum = 0.0 + for i in range(0, len(self._Nr)): + prob_sum += self._Nr[i] * self._prob_measure(i) / self._renormal + print("Probability Sum:", prob_sum) + # assert prob_sum != 1.0, "probability sum should be one!" + + def discount(self): + """ + This function returns the total mass of probability transfers from the + seen samples to the unseen samples. + """ + return self.smoothedNr(1) / self._freqdist.N() + + def max(self): + return self._freqdist.max() + + def samples(self): + return self._freqdist.keys() + + def freqdist(self): + return self._freqdist + + def __repr__(self): + """ + Return a string representation of this ``ProbDist``. + + :rtype: str + """ + return "" % self._freqdist.N() + + +class MutableProbDist(ProbDistI): + """ + An mutable probdist where the probabilities may be easily modified. This + simply copies an existing probdist, storing the probability values in a + mutable dictionary and providing an update method. + """ + + def __init__(self, prob_dist, samples, store_logs=True): + """ + Creates the mutable probdist based on the given prob_dist and using + the list of samples given. These values are stored as log + probabilities if the store_logs flag is set. + + :param prob_dist: the distribution from which to garner the + probabilities + :type prob_dist: ProbDist + :param samples: the complete set of samples + :type samples: sequence of any + :param store_logs: whether to store the probabilities as logarithms + :type store_logs: bool + """ + self._samples = samples + self._sample_dict = {samples[i]: i for i in range(len(samples))} + self._data = array.array("d", [0.0]) * len(samples) + for i in range(len(samples)): + if store_logs: + self._data[i] = prob_dist.logprob(samples[i]) + else: + self._data[i] = prob_dist.prob(samples[i]) + self._logs = store_logs + + def max(self): + # inherit documentation + return max((p, v) for (v, p) in self._sample_dict.items())[1] + + def samples(self): + # inherit documentation + return self._samples + + def prob(self, sample): + # inherit documentation + i = self._sample_dict.get(sample) + if i is None: + return 0.0 + return 2 ** (self._data[i]) if self._logs else self._data[i] + + def logprob(self, sample): + # inherit documentation + i = self._sample_dict.get(sample) + if i is None: + return float("-inf") + return self._data[i] if self._logs else math.log(self._data[i], 2) + + def update(self, sample, prob, log=True): + """ + Update the probability for the given sample. This may cause the object + to stop being the valid probability distribution - the user must + ensure that they update the sample probabilities such that all samples + have probabilities between 0 and 1 and that all probabilities sum to + one. + + :param sample: the sample for which to update the probability + :type sample: any + :param prob: the new probability + :type prob: float + :param log: is the probability already logged + :type log: bool + """ + i = self._sample_dict.get(sample) + assert i is not None + if self._logs: + self._data[i] = prob if log else math.log(prob, 2) + else: + self._data[i] = 2 ** (prob) if log else prob + + +##///////////////////////////////////////////////////// +## Kneser-Ney Probability Distribution +##////////////////////////////////////////////////////// + +# This method for calculating probabilities was introduced in 1995 by Reinhard +# Kneser and Hermann Ney. It was meant to improve the accuracy of language +# models that use backing-off to deal with sparse data. The authors propose two +# ways of doing so: a marginal distribution constraint on the back-off +# distribution and a leave-one-out distribution. For a start, the first one is +# implemented as a class below. +# +# The idea behind a back-off n-gram model is that we have a series of +# frequency distributions for our n-grams so that in case we have not seen a +# given n-gram during training (and as a result have a 0 probability for it) we +# can 'back off' (hence the name!) and try testing whether we've seen the +# n-1-gram part of the n-gram in training. +# +# The novelty of Kneser and Ney's approach was that they decided to fiddle +# around with the way this latter, backed off probability was being calculated +# whereas their peers seemed to focus on the primary probability. +# +# The implementation below uses one of the techniques described in their paper +# titled "Improved backing-off for n-gram language modeling." In the same paper +# another technique is introduced to attempt to smooth the back-off +# distribution as well as the primary one. There is also a much-cited +# modification of this method proposed by Chen and Goodman. +# +# In order for the implementation of Kneser-Ney to be more efficient, some +# changes have been made to the original algorithm. Namely, the calculation of +# the normalizing function gamma has been significantly simplified and +# combined slightly differently with beta. None of these changes affect the +# nature of the algorithm, but instead aim to cut out unnecessary calculations +# and take advantage of storing and retrieving information in dictionaries +# where possible. + + +class KneserNeyProbDist(ProbDistI): + """ + Kneser-Ney estimate of a probability distribution. This is a version of + back-off that counts how likely an n-gram is provided the n-1-gram had + been seen in training. Extends the ProbDistI interface, requires a trigram + FreqDist instance to train on. Optionally, a different from default discount + value can be specified. The default discount is set to 0.75. + + """ + + def __init__(self, freqdist, bins=None, discount=0.75): + """ + :param freqdist: The trigram frequency distribution upon which to base + the estimation + :type freqdist: FreqDist + :param bins: Included for compatibility with nltk.tag.hmm + :type bins: int or float + :param discount: The discount applied when retrieving counts of + trigrams + :type discount: float (preferred, but can be set to int) + """ + + if not bins: + self._bins = freqdist.B() + else: + self._bins = bins + self._D = discount + + # cache for probability calculation + self._cache = {} + + # internal bigram and trigram frequency distributions + self._bigrams = defaultdict(int) + self._trigrams = freqdist + + # helper dictionaries used to calculate probabilities + self._wordtypes_after = defaultdict(float) + self._trigrams_contain = defaultdict(float) + self._wordtypes_before = defaultdict(float) + for w0, w1, w2 in freqdist: + self._bigrams[(w0, w1)] += freqdist[(w0, w1, w2)] + self._wordtypes_after[(w0, w1)] += 1 + self._trigrams_contain[w1] += 1 + self._wordtypes_before[(w1, w2)] += 1 + + def prob(self, trigram): + # sample must be a triple + if len(trigram) != 3: + raise ValueError("Expected an iterable with 3 members.") + trigram = tuple(trigram) + w0, w1, w2 = trigram + + if trigram in self._cache: + return self._cache[trigram] + else: + # if the sample trigram was seen during training + if trigram in self._trigrams: + prob = (self._trigrams[trigram] - self.discount()) / self._bigrams[ + (w0, w1) + ] + + # else if the 'rougher' environment was seen during training + elif (w0, w1) in self._bigrams and (w1, w2) in self._wordtypes_before: + aftr = self._wordtypes_after[(w0, w1)] + bfr = self._wordtypes_before[(w1, w2)] + + # the probability left over from alphas + leftover_prob = (aftr * self.discount()) / self._bigrams[(w0, w1)] + + # the beta (including normalization) + beta = bfr / (self._trigrams_contain[w1] - aftr) + + prob = leftover_prob * beta + + # else the sample was completely unseen during training + else: + prob = 0.0 + + self._cache[trigram] = prob + return prob + + def discount(self): + """ + Return the value by which counts are discounted. By default set to 0.75. + + :rtype: float + """ + return self._D + + def set_discount(self, discount): + """ + Set the value by which counts are discounted to the value of discount. + + :param discount: the new value to discount counts by + :type discount: float (preferred, but int possible) + :rtype: None + """ + self._D = discount + + def samples(self): + return self._trigrams.keys() + + def max(self): + return self._trigrams.max() + + def __repr__(self): + """ + Return a string representation of this ProbDist + + :rtype: str + """ + return f">> from nltk.probability import ConditionalFreqDist + >>> from nltk.tokenize import word_tokenize + >>> sent = "the the the dog dog some other words that we do not care about" + >>> cfdist = ConditionalFreqDist() + >>> for word in word_tokenize(sent): + ... condition = len(word) + ... cfdist[condition][word] += 1 + + An equivalent way to do this is with the initializer: + + >>> cfdist = ConditionalFreqDist((len(word), word) for word in word_tokenize(sent)) + + The frequency distribution for each condition is accessed using + the indexing operator: + + >>> cfdist[3] + FreqDist({'the': 3, 'dog': 2, 'not': 1}) + >>> cfdist[3].freq('the') + 0.5 + >>> cfdist[3]['dog'] + 2 + + When the indexing operator is used to access the frequency + distribution for a condition that has not been accessed before, + ``ConditionalFreqDist`` creates a new empty FreqDist for that + condition. + + """ + + def __init__(self, cond_samples=None): + """ + Construct a new empty conditional frequency distribution. In + particular, the count for every sample, under every condition, + is zero. + + :param cond_samples: The samples to initialize the conditional + frequency distribution with + :type cond_samples: Sequence of (condition, sample) tuples + """ + defaultdict.__init__(self, FreqDist) + + if cond_samples: + for cond, sample in cond_samples: + self[cond][sample] += 1 + + def __reduce__(self): + kv_pairs = ((cond, self[cond]) for cond in self.conditions()) + return (self.__class__, (), None, None, kv_pairs) + + def conditions(self): + """ + Return a list of the conditions that have been accessed for + this ``ConditionalFreqDist``. Use the indexing operator to + access the frequency distribution for a given condition. + Note that the frequency distributions for some conditions + may contain zero sample outcomes. + + :rtype: list + """ + return list(self.keys()) + + def N(self): + """ + Return the total number of sample outcomes that have been + recorded by this ``ConditionalFreqDist``. + + :rtype: int + """ + return sum(fdist.N() for fdist in self.values()) + + def plot( + self, + *args, + samples=None, + title="", + cumulative=False, + percents=False, + conditions=None, + show=False, + **kwargs, + ): + """ + Plot the given samples from the conditional frequency distribution. + For a cumulative plot, specify cumulative=True. Additional ``*args`` and + ``**kwargs`` are passed to matplotlib's plot function. + (Requires Matplotlib to be installed.) + + :param samples: The samples to plot + :type samples: list + :param title: The title for the graph + :type title: str + :param cumulative: Whether the plot is cumulative. (default = False) + :type cumulative: bool + :param percents: Whether the plot uses percents instead of counts. (default = False) + :type percents: bool + :param conditions: The conditions to plot (default is all) + :type conditions: list + :param show: Whether to show the plot, or only return the ax. + :type show: bool + """ + try: + import matplotlib.pyplot as plt # import statement fix + except ImportError as e: + raise ValueError( + "The plot function requires matplotlib to be installed." + "See https://matplotlib.org/" + ) from e + + if not conditions: + conditions = self.conditions() + else: + conditions = [c for c in conditions if c in self] + if not samples: + samples = sorted({v for c in conditions for v in self[c]}) + if "linewidth" not in kwargs: + kwargs["linewidth"] = 2 + ax = plt.gca() + if conditions: + freqs = [] + for condition in conditions: + if cumulative: + # freqs should be a list of list where each sub list will be a frequency of a condition + freq = list(self[condition]._cumulative_frequencies(samples)) + else: + freq = [self[condition][sample] for sample in samples] + + if percents: + freq = [f / self[condition].N() * 100 for f in freq] + + freqs.append(freq) + + if cumulative: + ylabel = "Cumulative " + legend_loc = "lower right" + else: + ylabel = "" + legend_loc = "upper right" + + if percents: + ylabel += "Percents" + else: + ylabel += "Counts" + + i = 0 + for freq in freqs: + kwargs["label"] = conditions[i] # label for each condition + i += 1 + ax.plot(freq, *args, **kwargs) + ax.legend(loc=legend_loc) + ax.grid(True, color="silver") + ax.set_xticks(range(len(samples))) + ax.set_xticklabels([str(s) for s in samples], rotation=90) + if title: + ax.set_title(title) + ax.set_xlabel("Samples") + ax.set_ylabel(ylabel) + + if show: + plt.show() + + return ax + + def tabulate(self, *args, **kwargs): + """ + Tabulate the given samples from the conditional frequency distribution. + + :param samples: The samples to plot + :type samples: list + :param conditions: The conditions to plot (default is all) + :type conditions: list + :param cumulative: A flag to specify whether the freqs are cumulative (default = False) + :type title: bool + """ + + cumulative = _get_kwarg(kwargs, "cumulative", False) + conditions = _get_kwarg(kwargs, "conditions", sorted(self.conditions())) + samples = _get_kwarg( + kwargs, + "samples", + sorted({v for c in conditions if c in self for v in self[c]}), + ) # this computation could be wasted + + width = max(len("%s" % s) for s in samples) + freqs = dict() + for c in conditions: + if cumulative: + freqs[c] = list(self[c]._cumulative_frequencies(samples)) + else: + freqs[c] = [self[c][sample] for sample in samples] + width = max(width, max(len("%d" % f) for f in freqs[c])) + + condition_size = max(len("%s" % c) for c in conditions) + print(" " * condition_size, end=" ") + for s in samples: + print("%*s" % (width, s), end=" ") + print() + for c in conditions: + print("%*s" % (condition_size, c), end=" ") + for f in freqs[c]: + print("%*d" % (width, f), end=" ") + print() + + # Mathematical operators + + def __add__(self, other): + """ + Add counts from two ConditionalFreqDists. + """ + if not isinstance(other, ConditionalFreqDist): + return NotImplemented + result = self.copy() + for cond in other.conditions(): + result[cond] += other[cond] + return result + + def __sub__(self, other): + """ + Subtract count, but keep only results with positive counts. + """ + if not isinstance(other, ConditionalFreqDist): + return NotImplemented + result = self.copy() + for cond in other.conditions(): + result[cond] -= other[cond] + if not result[cond]: + del result[cond] + return result + + def __or__(self, other): + """ + Union is the maximum of value in either of the input counters. + """ + if not isinstance(other, ConditionalFreqDist): + return NotImplemented + result = self.copy() + for cond in other.conditions(): + result[cond] |= other[cond] + return result + + def __and__(self, other): + """ + Intersection is the minimum of corresponding counts. + """ + if not isinstance(other, ConditionalFreqDist): + return NotImplemented + result = ConditionalFreqDist() + for cond in self.conditions(): + newfreqdist = self[cond] & other[cond] + if newfreqdist: + result[cond] = newfreqdist + return result + + # @total_ordering doesn't work here, since the class inherits from a builtin class + def __le__(self, other): + if not isinstance(other, ConditionalFreqDist): + raise_unorderable_types("<=", self, other) + return set(self.conditions()).issubset(other.conditions()) and all( + self[c] <= other[c] for c in self.conditions() + ) + + def __lt__(self, other): + if not isinstance(other, ConditionalFreqDist): + raise_unorderable_types("<", self, other) + return self <= other and self != other + + def __ge__(self, other): + if not isinstance(other, ConditionalFreqDist): + raise_unorderable_types(">=", self, other) + return other <= self + + def __gt__(self, other): + if not isinstance(other, ConditionalFreqDist): + raise_unorderable_types(">", self, other) + return other < self + + def deepcopy(self): + from copy import deepcopy + + return deepcopy(self) + + copy = deepcopy + + def __repr__(self): + """ + Return a string representation of this ``ConditionalFreqDist``. + + :rtype: str + """ + return "" % len(self) + + +class ConditionalProbDistI(dict, metaclass=ABCMeta): + """ + A collection of probability distributions for a single experiment + run under different conditions. Conditional probability + distributions are used to estimate the likelihood of each sample, + given the condition under which the experiment was run. For + example, a conditional probability distribution could be used to + estimate the probability of each word type in a document, given + the length of the word type. Formally, a conditional probability + distribution can be defined as a function that maps from each + condition to the ``ProbDist`` for the experiment under that + condition. + """ + + @abstractmethod + def __init__(self): + """ + Classes inheriting from ConditionalProbDistI should implement __init__. + """ + + def conditions(self): + """ + Return a list of the conditions that are represented by + this ``ConditionalProbDist``. Use the indexing operator to + access the probability distribution for a given condition. + + :rtype: list + """ + return list(self.keys()) + + def __repr__(self): + """ + Return a string representation of this ``ConditionalProbDist``. + + :rtype: str + """ + return "<%s with %d conditions>" % (type(self).__name__, len(self)) + + +class ConditionalProbDist(ConditionalProbDistI): + """ + A conditional probability distribution modeling the experiments + that were used to generate a conditional frequency distribution. + A ConditionalProbDist is constructed from a + ``ConditionalFreqDist`` and a ``ProbDist`` factory: + + - The ``ConditionalFreqDist`` specifies the frequency + distribution for each condition. + - The ``ProbDist`` factory is a function that takes a + condition's frequency distribution, and returns its + probability distribution. A ``ProbDist`` class's name (such as + ``MLEProbDist`` or ``HeldoutProbDist``) can be used to specify + that class's constructor. + + The first argument to the ``ProbDist`` factory is the frequency + distribution that it should model; and the remaining arguments are + specified by the ``factory_args`` parameter to the + ``ConditionalProbDist`` constructor. For example, the following + code constructs a ``ConditionalProbDist``, where the probability + distribution for each condition is an ``ELEProbDist`` with 10 bins: + + >>> from nltk.corpus import brown + >>> from nltk.probability import ConditionalFreqDist + >>> from nltk.probability import ConditionalProbDist, ELEProbDist + >>> cfdist = ConditionalFreqDist(brown.tagged_words()[:5000]) + >>> cpdist = ConditionalProbDist(cfdist, ELEProbDist, 10) + >>> cpdist['passed'].max() + 'VBD' + >>> cpdist['passed'].prob('VBD') #doctest: +ELLIPSIS + 0.423... + + """ + + def __init__(self, cfdist, probdist_factory, *factory_args, **factory_kw_args): + """ + Construct a new conditional probability distribution, based on + the given conditional frequency distribution and ``ProbDist`` + factory. + + :type cfdist: ConditionalFreqDist + :param cfdist: The ``ConditionalFreqDist`` specifying the + frequency distribution for each condition. + :type probdist_factory: class or function + :param probdist_factory: The function or class that maps + a condition's frequency distribution to its probability + distribution. The function is called with the frequency + distribution as its first argument, + ``factory_args`` as its remaining arguments, and + ``factory_kw_args`` as keyword arguments. + :type factory_args: (any) + :param factory_args: Extra arguments for ``probdist_factory``. + These arguments are usually used to specify extra + properties for the probability distributions of individual + conditions, such as the number of bins they contain. + :type factory_kw_args: (any) + :param factory_kw_args: Extra keyword arguments for ``probdist_factory``. + """ + self._probdist_factory = probdist_factory + self._factory_args = factory_args + self._factory_kw_args = factory_kw_args + + for condition in cfdist: + self[condition] = probdist_factory( + cfdist[condition], *factory_args, **factory_kw_args + ) + + def __missing__(self, key): + self[key] = self._probdist_factory( + FreqDist(), *self._factory_args, **self._factory_kw_args + ) + return self[key] + + +class DictionaryConditionalProbDist(ConditionalProbDistI): + """ + An alternative ConditionalProbDist that simply wraps a dictionary of + ProbDists rather than creating these from FreqDists. + """ + + def __init__(self, probdist_dict): + """ + :param probdist_dict: a dictionary containing the probdists indexed + by the conditions + :type probdist_dict: dict any -> probdist + """ + self.update(probdist_dict) + + def __missing__(self, key): + self[key] = DictionaryProbDist() + return self[key] + + +##////////////////////////////////////////////////////// +## Adding in log-space. +##////////////////////////////////////////////////////// + +# If the difference is bigger than this, then just take the bigger one: +_ADD_LOGS_MAX_DIFF = math.log(1e-30, 2) + + +def add_logs(logx, logy): + """ + Given two numbers ``logx`` = *log(x)* and ``logy`` = *log(y)*, return + *log(x+y)*. Conceptually, this is the same as returning + ``log(2**(logx)+2**(logy))``, but the actual implementation + avoids overflow errors that could result from direct computation. + """ + if logx < logy + _ADD_LOGS_MAX_DIFF: + return logy + if logy < logx + _ADD_LOGS_MAX_DIFF: + return logx + base = min(logx, logy) + return base + math.log(2 ** (logx - base) + 2 ** (logy - base), 2) + + +def sum_logs(logs): + return reduce(add_logs, logs[1:], logs[0]) if len(logs) != 0 else _NINF + + +##////////////////////////////////////////////////////// +## Probabilistic Mix-in +##////////////////////////////////////////////////////// + + +class ProbabilisticMixIn: + """ + A mix-in class to associate probabilities with other classes + (trees, rules, etc.). To use the ``ProbabilisticMixIn`` class, + define a new class that derives from an existing class and from + ProbabilisticMixIn. You will need to define a new constructor for + the new class, which explicitly calls the constructors of both its + parent classes. For example: + + >>> from nltk.probability import ProbabilisticMixIn + >>> class A: + ... def __init__(self, x, y): self.data = (x,y) + ... + >>> class ProbabilisticA(A, ProbabilisticMixIn): + ... def __init__(self, x, y, **prob_kwarg): + ... A.__init__(self, x, y) + ... ProbabilisticMixIn.__init__(self, **prob_kwarg) + + See the documentation for the ProbabilisticMixIn + ``constructor<__init__>`` for information about the arguments it + expects. + + You should generally also redefine the string representation + methods, the comparison methods, and the hashing method. + """ + + def __init__(self, **kwargs): + """ + Initialize this object's probability. This initializer should + be called by subclass constructors. ``prob`` should generally be + the first argument for those constructors. + + :param prob: The probability associated with the object. + :type prob: float + :param logprob: The log of the probability associated with + the object. + :type logprob: float + """ + if "prob" in kwargs: + if "logprob" in kwargs: + raise TypeError("Must specify either prob or logprob " "(not both)") + else: + ProbabilisticMixIn.set_prob(self, kwargs["prob"]) + elif "logprob" in kwargs: + ProbabilisticMixIn.set_logprob(self, kwargs["logprob"]) + else: + self.__prob = self.__logprob = None + + def set_prob(self, prob): + """ + Set the probability associated with this object to ``prob``. + + :param prob: The new probability + :type prob: float + """ + self.__prob = prob + self.__logprob = None + + def set_logprob(self, logprob): + """ + Set the log probability associated with this object to + ``logprob``. I.e., set the probability associated with this + object to ``2**(logprob)``. + + :param logprob: The new log probability + :type logprob: float + """ + self.__logprob = logprob + self.__prob = None + + def prob(self): + """ + Return the probability associated with this object. + + :rtype: float + """ + if self.__prob is None: + if self.__logprob is None: + return None + self.__prob = 2 ** (self.__logprob) + return self.__prob + + def logprob(self): + """ + Return ``log(p)``, where ``p`` is the probability associated + with this object. + + :rtype: float + """ + if self.__logprob is None: + if self.__prob is None: + return None + self.__logprob = math.log(self.__prob, 2) + return self.__logprob + + +class ImmutableProbabilisticMixIn(ProbabilisticMixIn): + def set_prob(self, prob): + raise ValueError("%s is immutable" % self.__class__.__name__) + + def set_logprob(self, prob): + raise ValueError("%s is immutable" % self.__class__.__name__) + + +## Helper function for processing keyword arguments + + +def _get_kwarg(kwargs, key, default): + if key in kwargs: + arg = kwargs[key] + del kwargs[key] + else: + arg = default + return arg + + +##////////////////////////////////////////////////////// +## Demonstration +##////////////////////////////////////////////////////// + + +def _create_rand_fdist(numsamples, numoutcomes): + """ + Create a new frequency distribution, with random samples. The + samples are numbers from 1 to ``numsamples``, and are generated by + summing two numbers, each of which has a uniform distribution. + """ + + fdist = FreqDist() + for x in range(numoutcomes): + y = random.randint(1, (1 + numsamples) // 2) + random.randint( + 0, numsamples // 2 + ) + fdist[y] += 1 + return fdist + + +def _create_sum_pdist(numsamples): + """ + Return the true probability distribution for the experiment + ``_create_rand_fdist(numsamples, x)``. + """ + fdist = FreqDist() + for x in range(1, (1 + numsamples) // 2 + 1): + for y in range(0, numsamples // 2 + 1): + fdist[x + y] += 1 + return MLEProbDist(fdist) + + +def demo(numsamples=6, numoutcomes=500): + """ + A demonstration of frequency distributions and probability + distributions. This demonstration creates three frequency + distributions with, and uses them to sample a random process with + ``numsamples`` samples. Each frequency distribution is sampled + ``numoutcomes`` times. These three frequency distributions are + then used to build six probability distributions. Finally, the + probability estimates of these distributions are compared to the + actual probability of each sample. + + :type numsamples: int + :param numsamples: The number of samples to use in each demo + frequency distributions. + :type numoutcomes: int + :param numoutcomes: The total number of outcomes for each + demo frequency distribution. These outcomes are divided into + ``numsamples`` bins. + :rtype: None + """ + + # Randomly sample a stochastic process three times. + fdist1 = _create_rand_fdist(numsamples, numoutcomes) + fdist2 = _create_rand_fdist(numsamples, numoutcomes) + fdist3 = _create_rand_fdist(numsamples, numoutcomes) + + # Use our samples to create probability distributions. + pdists = [ + MLEProbDist(fdist1), + LidstoneProbDist(fdist1, 0.5, numsamples), + HeldoutProbDist(fdist1, fdist2, numsamples), + HeldoutProbDist(fdist2, fdist1, numsamples), + CrossValidationProbDist([fdist1, fdist2, fdist3], numsamples), + SimpleGoodTuringProbDist(fdist1), + SimpleGoodTuringProbDist(fdist1, 7), + _create_sum_pdist(numsamples), + ] + + # Find the probability of each sample. + vals = [] + for n in range(1, numsamples + 1): + vals.append(tuple([n, fdist1.freq(n)] + [pdist.prob(n) for pdist in pdists])) + + # Print the results in a formatted table. + print( + "%d samples (1-%d); %d outcomes were sampled for each FreqDist" + % (numsamples, numsamples, numoutcomes) + ) + print("=" * 9 * (len(pdists) + 2)) + FORMATSTR = " FreqDist " + "%8s " * (len(pdists) - 1) + "| Actual" + print(FORMATSTR % tuple(repr(pdist)[1:9] for pdist in pdists[:-1])) + print("-" * 9 * (len(pdists) + 2)) + FORMATSTR = "%3d %8.6f " + "%8.6f " * (len(pdists) - 1) + "| %8.6f" + for val in vals: + print(FORMATSTR % val) + + # Print the totals for each column (should all be 1.0) + zvals = list(zip(*vals)) + sums = [sum(val) for val in zvals[1:]] + print("-" * 9 * (len(pdists) + 2)) + FORMATSTR = "Total " + "%8.6f " * (len(pdists)) + "| %8.6f" + print(FORMATSTR % tuple(sums)) + print("=" * 9 * (len(pdists) + 2)) + + # Display the distributions themselves, if they're short enough. + if len("%s" % fdist1) < 70: + print(" fdist1: %s" % fdist1) + print(" fdist2: %s" % fdist2) + print(" fdist3: %s" % fdist3) + print() + + print("Generating:") + for pdist in pdists: + fdist = FreqDist(pdist.generate() for i in range(5000)) + print("{:>20} {}".format(pdist.__class__.__name__[:20], ("%s" % fdist)[:55])) + print() + + +def gt_demo(): + from nltk import corpus + + emma_words = corpus.gutenberg.words("austen-emma.txt") + fd = FreqDist(emma_words) + sgt = SimpleGoodTuringProbDist(fd) + print("{:>18} {:>8} {:>14}".format("word", "frequency", "SimpleGoodTuring")) + fd_keys_sorted = ( + key for key, value in sorted(fd.items(), key=lambda item: item[1], reverse=True) + ) + for key in fd_keys_sorted: + print("%18s %8d %14e" % (key, fd[key], sgt.prob(key))) + + +if __name__ == "__main__": + demo(6, 10) + demo(5, 5000) + gt_demo() + +__all__ = [ + "ConditionalFreqDist", + "ConditionalProbDist", + "ConditionalProbDistI", + "CrossValidationProbDist", + "DictionaryConditionalProbDist", + "DictionaryProbDist", + "ELEProbDist", + "FreqDist", + "SimpleGoodTuringProbDist", + "HeldoutProbDist", + "ImmutableProbabilisticMixIn", + "LaplaceProbDist", + "LidstoneProbDist", + "MLEProbDist", + "MutableProbDist", + "KneserNeyProbDist", + "ProbDistI", + "ProbabilisticMixIn", + "UniformProbDist", + "WittenBellProbDist", + "add_logs", + "log_likelihood", + "sum_logs", + "entropy", +] diff --git a/nltk/sem/__init__.py b/nltk/sem/__init__.py new file mode 100644 index 0000000..9386d9c --- /dev/null +++ b/nltk/sem/__init__.py @@ -0,0 +1,75 @@ +# Natural Language Toolkit: Semantic Interpretation +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +""" +NLTK Semantic Interpretation Package + +This package contains classes for representing semantic structure in +formulas of first-order logic and for evaluating such formulas in +set-theoretic models. + + >>> from nltk.sem import logic + >>> logic._counter._value = 0 + +The package has two main components: + + - ``logic`` provides support for analyzing expressions of First + Order Logic (FOL). + - ``evaluate`` allows users to recursively determine truth in a + model for formulas of FOL. + +A model consists of a domain of discourse and a valuation function, +which assigns values to non-logical constants. We assume that entities +in the domain are represented as strings such as ``'b1'``, ``'g1'``, +etc. A ``Valuation`` is initialized with a list of (symbol, value) +pairs, where values are entities, sets of entities or sets of tuples +of entities. +The domain of discourse can be inferred from the valuation, and model +is then created with domain and valuation as parameters. + + >>> from nltk.sem import Valuation, Model + >>> v = [('adam', 'b1'), ('betty', 'g1'), ('fido', 'd1'), + ... ('girl', set(['g1', 'g2'])), ('boy', set(['b1', 'b2'])), + ... ('dog', set(['d1'])), + ... ('love', set([('b1', 'g1'), ('b2', 'g2'), ('g1', 'b1'), ('g2', 'b1')]))] + >>> val = Valuation(v) + >>> dom = val.domain + >>> m = Model(dom, val) +""" + +from nltk.sem.boxer import Boxer +from nltk.sem.drt import DRS, DrtExpression +from nltk.sem.evaluate import ( + Assignment, + Model, + Undefined, + Valuation, + arity, + is_rel, + read_valuation, + set2rel, +) +from nltk.sem.lfg import FStructure +from nltk.sem.logic import ( + ApplicationExpression, + Expression, + LogicalExpressionException, + Variable, + binding_ops, + boolean_ops, + equality_preds, + read_logic, +) +from nltk.sem.relextract import clause, extract_rels, rtuple +from nltk.sem.skolemize import skolemize +from nltk.sem.util import evaluate_sents, interpret_sents, parse_sents, root_semrep + +# from nltk.sem.glue import Glue +# from nltk.sem.hole import HoleSemantics +# from nltk.sem.cooper_storage import CooperStore + +# don't import chat80 as its names are too generic diff --git a/nltk/sem/boxer.py b/nltk/sem/boxer.py new file mode 100644 index 0000000..e6c8c4f --- /dev/null +++ b/nltk/sem/boxer.py @@ -0,0 +1,1632 @@ +# Natural Language Toolkit: Interface to Boxer +# +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +An interface to Boxer. + +This interface relies on the latest version of the development (subversion) version of +C&C and Boxer. + +Usage +===== + +Set the environment variable CANDC to the bin directory of your CandC installation. +The models directory should be in the CandC root directory. +For example:: + + /path/to/candc/ + bin/ + candc + boxer + models/ + boxer/ +""" + +import operator +import os +import re +import subprocess +import tempfile +from functools import reduce +from optparse import OptionParser + +from nltk.internals import find_binary_iter +from nltk.sem.drt import ( + DRS, + DrtApplicationExpression, + DrtEqualityExpression, + DrtNegatedExpression, + DrtOrExpression, + DrtParser, + DrtProposition, + DrtTokens, + DrtVariableExpression, +) +from nltk.sem.logic import ( + ExpectedMoreTokensException, + LogicalExpressionException, + UnexpectedTokenException, + Variable, +) + + +class Boxer: + """ + This class is an interface to Johan Bos's program Boxer, a wide-coverage + semantic parser that produces Discourse Representation Structures (DRSs). + """ + + def __init__( + self, + boxer_drs_interpreter=None, + elimeq=False, + bin_dir=None, + verbose=False, + resolve=True, + ): + """ + :param boxer_drs_interpreter: A class that converts from the + ``AbstractBoxerDrs`` object hierarchy to a different object. The + default is ``NltkDrtBoxerDrsInterpreter``, which converts to the NLTK + DRT hierarchy. + :param elimeq: When set to true, Boxer removes all equalities from the + DRSs and discourse referents standing in the equality relation are + unified, but only if this can be done in a meaning-preserving manner. + :param resolve: When set to true, Boxer will resolve all anaphoric DRSs and perform merge-reduction. + Resolution follows Van der Sandt's theory of binding and accommodation. + """ + if boxer_drs_interpreter is None: + boxer_drs_interpreter = NltkDrtBoxerDrsInterpreter() + self._boxer_drs_interpreter = boxer_drs_interpreter + + self._resolve = resolve + self._elimeq = elimeq + + self.set_bin_dir(bin_dir, verbose) + + def set_bin_dir(self, bin_dir, verbose=False): + self._candc_bin = self._find_binary("candc", bin_dir, verbose) + self._candc_models_path = os.path.normpath( + os.path.join(self._candc_bin[:-5], "../models") + ) + self._boxer_bin = self._find_binary("boxer", bin_dir, verbose) + + def interpret(self, input, discourse_id=None, question=False, verbose=False): + """ + Use Boxer to give a first order representation. + + :param input: str Input sentence to parse + :param occur_index: bool Should predicates be occurrence indexed? + :param discourse_id: str An identifier to be inserted to each occurrence-indexed predicate. + :return: ``drt.DrtExpression`` + """ + discourse_ids = [discourse_id] if discourse_id is not None else None + (d,) = self.interpret_multi_sents([[input]], discourse_ids, question, verbose) + if not d: + raise Exception(f'Unable to interpret: "{input}"') + return d + + def interpret_multi(self, input, discourse_id=None, question=False, verbose=False): + """ + Use Boxer to give a first order representation. + + :param input: list of str Input sentences to parse as a single discourse + :param occur_index: bool Should predicates be occurrence indexed? + :param discourse_id: str An identifier to be inserted to each occurrence-indexed predicate. + :return: ``drt.DrtExpression`` + """ + discourse_ids = [discourse_id] if discourse_id is not None else None + (d,) = self.interpret_multi_sents([input], discourse_ids, question, verbose) + if not d: + raise Exception(f'Unable to interpret: "{input}"') + return d + + def interpret_sents( + self, inputs, discourse_ids=None, question=False, verbose=False + ): + """ + Use Boxer to give a first order representation. + + :param inputs: list of str Input sentences to parse as individual discourses + :param occur_index: bool Should predicates be occurrence indexed? + :param discourse_ids: list of str Identifiers to be inserted to each occurrence-indexed predicate. + :return: list of ``drt.DrtExpression`` + """ + return self.interpret_multi_sents( + [[input] for input in inputs], discourse_ids, question, verbose + ) + + def interpret_multi_sents( + self, inputs, discourse_ids=None, question=False, verbose=False + ): + """ + Use Boxer to give a first order representation. + + :param inputs: list of list of str Input discourses to parse + :param occur_index: bool Should predicates be occurrence indexed? + :param discourse_ids: list of str Identifiers to be inserted to each occurrence-indexed predicate. + :return: ``drt.DrtExpression`` + """ + if discourse_ids is not None: + assert len(inputs) == len(discourse_ids) + assert reduce(operator.and_, (id is not None for id in discourse_ids)) + use_disc_id = True + else: + discourse_ids = list(map(str, range(len(inputs)))) + use_disc_id = False + + candc_out = self._call_candc(inputs, discourse_ids, question, verbose=verbose) + boxer_out = self._call_boxer(candc_out, verbose=verbose) + + # if 'ERROR: input file contains no ccg/2 terms.' in boxer_out: + # raise UnparseableInputException('Could not parse with candc: "%s"' % input_str) + + drs_dict = self._parse_to_drs_dict(boxer_out, use_disc_id) + return [drs_dict.get(id, None) for id in discourse_ids] + + def _call_candc(self, inputs, discourse_ids, question, verbose=False): + """ + Call the ``candc`` binary with the given input. + + :param inputs: list of list of str Input discourses to parse + :param discourse_ids: list of str Identifiers to be inserted to each occurrence-indexed predicate. + :param filename: str A filename for the output file + :return: stdout + """ + args = [ + "--models", + os.path.join(self._candc_models_path, ["boxer", "questions"][question]), + "--candc-printer", + "boxer", + ] + return self._call( + "\n".join( + sum( + ([f"'{id}'"] + d for d, id in zip(inputs, discourse_ids)), + [], + ) + ), + self._candc_bin, + args, + verbose, + ) + + def _call_boxer(self, candc_out, verbose=False): + """ + Call the ``boxer`` binary with the given input. + + :param candc_out: str output from C&C parser + :return: stdout + """ + f = None + try: + fd, temp_filename = tempfile.mkstemp( + prefix="boxer-", suffix=".in", text=True + ) + f = os.fdopen(fd, "w") + f.write(candc_out.decode("utf-8")) + finally: + if f: + f.close() + + args = [ + "--box", + "false", + "--semantics", + "drs", + #'--flat', 'false', # removed from boxer + "--resolve", + ["false", "true"][self._resolve], + "--elimeq", + ["false", "true"][self._elimeq], + "--format", + "prolog", + "--instantiate", + "true", + "--input", + temp_filename, + ] + stdout = self._call(None, self._boxer_bin, args, verbose) + os.remove(temp_filename) + return stdout + + def _find_binary(self, name, bin_dir, verbose=False): + # find_binary() also matches a binary relative to the current working + # directory: a relative bin_dir yields e.g. "./candc", and even the + # default bin_dir=None matches a "/" directory in the CWD + # (find_file_iter joins the name with itself). Such a result contains a + # path separator, so _call()'s subprocess.Popen() would execute it + # directly from the CWD without consulting $PATH -- an attacker who can + # plant a "candc"/"boxer" file there would get code execution (an + # untrusted search path, CWE-426/CWE-427). Skip any CWD-relative + # candidate and use the first absolute one (an absolute bin_dir, the + # CANDC environment variable, or a $PATH lookup), none of which resolve + # against the CWD. + for binary in find_binary_iter( + name, + path_to_bin=bin_dir, + env_vars=["CANDC"], + url="http://svn.ask.it.usyd.edu.au/trac/candc/", + binary_names=[name, name + ".exe"], + verbose=verbose, + ): + if os.path.isabs(binary): + return binary + raise LookupError( + "No absolute %r binary found. Pass an absolute bin_dir to Boxer(...) " + "or set the CANDC environment variable to an absolute directory that " + "contains the candc/boxer binaries (a binary found relative to the " + "current working directory is refused)." % name + ) + + def _call(self, input_str, binary, args=[], verbose=False): + """ + Call the binary with the given input. + + :param input_str: A string whose contents are used as stdin. + :param binary: The location of the binary to call + :param args: A list of command-line arguments. + :return: stdout + """ + if verbose: + print("Calling:", binary) + print("Args:", args) + print("Input:", input_str) + print("Command:", binary + " " + " ".join(args)) + + # Call via a subprocess + if input_str is None: + cmd = [binary] + args + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + else: + cmd = [binary] + args + p = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + stdout, stderr = p.communicate(input=input_str.encode("utf-8")) + + if verbose: + print("Return code:", p.returncode) + if stdout: + print("stdout:\n", stdout, "\n") + if stderr: + print("stderr:\n", stderr, "\n") + if p.returncode != 0: + raise Exception( + "ERROR CALLING: {} {}\nReturncode: {}\n{}".format( + binary, " ".join(args), p.returncode, stderr + ) + ) + + return stdout + + def _parse_to_drs_dict(self, boxer_out, use_disc_id): + lines = boxer_out.decode("utf-8").split("\n") + drs_dict = {} + i = 0 + while i < len(lines): + line = lines[i] + if line.startswith("id("): + comma_idx = line.index(",") + discourse_id = line[3:comma_idx] + if discourse_id[0] == "'" and discourse_id[-1] == "'": + discourse_id = discourse_id[1:-1] + drs_id = line[comma_idx + 1 : line.index(")")] + i += 1 + line = lines[i] + assert line.startswith(f"sem({drs_id},") + if line[-4:] == "').'": + line = line[:-4] + ")." + assert line.endswith(")."), f"can't parse line: {line}" + + search_start = len(f"sem({drs_id},[") + brace_count = 1 + drs_start = -1 + for j, c in enumerate(line[search_start:]): + if c == "[": + brace_count += 1 + if c == "]": + brace_count -= 1 + if brace_count == 0: + drs_start = search_start + j + 1 + if line[drs_start : drs_start + 3] == "','": + drs_start = drs_start + 3 + else: + drs_start = drs_start + 1 + break + assert drs_start > -1 + + drs_input = line[drs_start:-2].strip() + parsed = self._parse_drs(drs_input, discourse_id, use_disc_id) + drs_dict[discourse_id] = self._boxer_drs_interpreter.interpret(parsed) + i += 1 + return drs_dict + + def _parse_drs(self, drs_string, discourse_id, use_disc_id): + return BoxerOutputDrsParser([None, discourse_id][use_disc_id]).parse(drs_string) + + +class BoxerOutputDrsParser(DrtParser): + def __init__(self, discourse_id=None): + """ + This class is used to parse the Prolog DRS output from Boxer into a + hierarchy of python objects. + """ + DrtParser.__init__(self) + self.discourse_id = discourse_id + self.sentence_id_offset = None + self.quote_chars = [("'", "'", "\\", False)] + + def parse(self, data, signature=None): + return DrtParser.parse(self, data, signature) + + def get_all_symbols(self): + return ["(", ")", ",", "[", "]", ":"] + + def handle(self, tok, context): + return self.handle_drs(tok) + + def attempt_adjuncts(self, expression, context): + return expression + + def parse_condition(self, indices): + """ + Parse a DRS condition + + :return: list of ``DrtExpression`` + """ + tok = self.token() + accum = self.handle_condition(tok, indices) + if accum is None: + raise UnexpectedTokenException(tok) + return accum + + def handle_drs(self, tok): + if tok == "drs": + return self.parse_drs() + elif tok in ["merge", "smerge"]: + return self._handle_binary_expression(self._make_merge_expression)(None, []) + elif tok in ["alfa"]: + return self._handle_alfa(self._make_merge_expression)(None, []) + + def handle_condition(self, tok, indices): + """ + Handle a DRS condition + + :param indices: list of int + :return: list of ``DrtExpression`` + """ + if tok == "not": + return [self._handle_not()] + + if tok == "or": + conds = [self._handle_binary_expression(self._make_or_expression)] + elif tok == "imp": + conds = [self._handle_binary_expression(self._make_imp_expression)] + elif tok == "eq": + conds = [self._handle_eq()] + elif tok == "prop": + conds = [self._handle_prop()] + + elif tok == "pred": + conds = [self._handle_pred()] + elif tok == "named": + conds = [self._handle_named()] + elif tok == "rel": + conds = [self._handle_rel()] + elif tok == "timex": + conds = self._handle_timex() + elif tok == "card": + conds = [self._handle_card()] + + elif tok == "whq": + conds = [self._handle_whq()] + elif tok == "duplex": + conds = [self._handle_duplex()] + + else: + conds = [] + + return sum( + ( + [cond(sent_index, word_indices) for cond in conds] + for sent_index, word_indices in self._sent_and_word_indices(indices) + ), + [], + ) + + def _handle_not(self): + self.assertToken(self.token(), "(") + drs = self.process_next_expression(None) + self.assertToken(self.token(), ")") + return BoxerNot(drs) + + def _handle_pred(self): + # pred(_G3943, dog, n, 0) + self.assertToken(self.token(), "(") + variable = self.parse_variable() + self.assertToken(self.token(), ",") + name = self.token() + self.assertToken(self.token(), ",") + pos = self.token() + self.assertToken(self.token(), ",") + sense = int(self.token()) + self.assertToken(self.token(), ")") + + def _handle_pred_f(sent_index, word_indices): + return BoxerPred( + self.discourse_id, sent_index, word_indices, variable, name, pos, sense + ) + + return _handle_pred_f + + def _handle_duplex(self): + # duplex(whq, drs(...), var, drs(...)) + self.assertToken(self.token(), "(") + # self.assertToken(self.token(), '[') + ans_types = [] + # while self.token(0) != ']': + # cat = self.token() + # self.assertToken(self.token(), ':') + # if cat == 'des': + # ans_types.append(self.token()) + # elif cat == 'num': + # ans_types.append('number') + # typ = self.token() + # if typ == 'cou': + # ans_types.append('count') + # else: + # ans_types.append(typ) + # else: + # ans_types.append(self.token()) + # self.token() #swallow the ']' + + self.assertToken(self.token(), "whq") + self.assertToken(self.token(), ",") + d1 = self.process_next_expression(None) + self.assertToken(self.token(), ",") + ref = self.parse_variable() + self.assertToken(self.token(), ",") + d2 = self.process_next_expression(None) + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerWhq( + self.discourse_id, sent_index, word_indices, ans_types, d1, ref, d2 + ) + + def _handle_named(self): + # named(x0, john, per, 0) + self.assertToken(self.token(), "(") + variable = self.parse_variable() + self.assertToken(self.token(), ",") + name = self.token() + self.assertToken(self.token(), ",") + type = self.token() + self.assertToken(self.token(), ",") + sense = self.token() # as per boxer rev 2554 + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerNamed( + self.discourse_id, sent_index, word_indices, variable, name, type, sense + ) + + def _handle_rel(self): + # rel(_G3993, _G3943, agent, 0) + self.assertToken(self.token(), "(") + var1 = self.parse_variable() + self.assertToken(self.token(), ",") + var2 = self.parse_variable() + self.assertToken(self.token(), ",") + rel = self.token() + self.assertToken(self.token(), ",") + sense = int(self.token()) + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerRel( + self.discourse_id, sent_index, word_indices, var1, var2, rel, sense + ) + + def _handle_timex(self): + # timex(_G18322, date([]: (+), []:'XXXX', [1004]:'04', []:'XX')) + self.assertToken(self.token(), "(") + arg = self.parse_variable() + self.assertToken(self.token(), ",") + new_conds = self._handle_time_expression(arg) + self.assertToken(self.token(), ")") + return new_conds + + def _handle_time_expression(self, arg): + # date([]: (+), []:'XXXX', [1004]:'04', []:'XX') + tok = self.token() + self.assertToken(self.token(), "(") + if tok == "date": + conds = self._handle_date(arg) + elif tok == "time": + conds = self._handle_time(arg) + else: + return None + self.assertToken(self.token(), ")") + + def func_gen(x): + return lambda sent_index, word_indices: x + + return [ + lambda sent_index, word_indices: BoxerPred( + self.discourse_id, sent_index, word_indices, arg, tok, "n", 0 + ) + ] + [func_gen(cond) for cond in conds] + + def _handle_date(self, arg): + # []: (+), []:'XXXX', [1004]:'04', []:'XX' + conds = [] + ((sent_index, word_indices),) = self._sent_and_word_indices( + self._parse_index_list() + ) + self.assertToken(self.token(), "(") + pol = self.token() + self.assertToken(self.token(), ")") + conds.append( + BoxerPred( + self.discourse_id, + sent_index, + word_indices, + arg, + f"date_pol_{pol}", + "a", + 0, + ) + ) + self.assertToken(self.token(), ",") + + ((sent_index, word_indices),) = self._sent_and_word_indices( + self._parse_index_list() + ) + year = self.token() + if year != "XXXX": + year = year.replace(":", "_") + conds.append( + BoxerPred( + self.discourse_id, + sent_index, + word_indices, + arg, + f"date_year_{year}", + "a", + 0, + ) + ) + self.assertToken(self.token(), ",") + + ((sent_index, word_indices),) = self._sent_and_word_indices( + self._parse_index_list() + ) + month = self.token() + if month != "XX": + conds.append( + BoxerPred( + self.discourse_id, + sent_index, + word_indices, + arg, + f"date_month_{month}", + "a", + 0, + ) + ) + self.assertToken(self.token(), ",") + + ((sent_index, word_indices),) = self._sent_and_word_indices( + self._parse_index_list() + ) + day = self.token() + if day != "XX": + conds.append( + BoxerPred( + self.discourse_id, + sent_index, + word_indices, + arg, + f"date_day_{day}", + "a", + 0, + ) + ) + + return conds + + def _handle_time(self, arg): + # time([1018]:'18', []:'XX', []:'XX') + conds = [] + self._parse_index_list() + hour = self.token() + if hour != "XX": + conds.append(self._make_atom("r_hour_2", arg, hour)) + self.assertToken(self.token(), ",") + + self._parse_index_list() + min = self.token() + if min != "XX": + conds.append(self._make_atom("r_min_2", arg, min)) + self.assertToken(self.token(), ",") + + self._parse_index_list() + sec = self.token() + if sec != "XX": + conds.append(self._make_atom("r_sec_2", arg, sec)) + + return conds + + def _handle_card(self): + # card(_G18535, 28, ge) + self.assertToken(self.token(), "(") + variable = self.parse_variable() + self.assertToken(self.token(), ",") + value = self.token() + self.assertToken(self.token(), ",") + type = self.token() + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerCard( + self.discourse_id, sent_index, word_indices, variable, value, type + ) + + def _handle_prop(self): + # prop(_G15949, drs(...)) + self.assertToken(self.token(), "(") + variable = self.parse_variable() + self.assertToken(self.token(), ",") + drs = self.process_next_expression(None) + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerProp( + self.discourse_id, sent_index, word_indices, variable, drs + ) + + def _parse_index_list(self): + # [1001,1002]: + indices = [] + self.assertToken(self.token(), "[") + while self.token(0) != "]": + indices.append(self.parse_index()) + if self.token(0) == ",": + self.token() # swallow ',' + self.token() # swallow ']' + self.assertToken(self.token(), ":") + return indices + + def parse_drs(self): + # drs([[1001]:_G3943], + # [[1002]:pred(_G3943, dog, n, 0)] + # ) + self.assertToken(self.token(), "(") + self.assertToken(self.token(), "[") + refs = set() + while self.token(0) != "]": + indices = self._parse_index_list() + refs.add(self.parse_variable()) + if self.token(0) == ",": + self.token() # swallow ',' + self.token() # swallow ']' + self.assertToken(self.token(), ",") + self.assertToken(self.token(), "[") + conds = [] + while self.token(0) != "]": + indices = self._parse_index_list() + conds.extend(self.parse_condition(indices)) + if self.token(0) == ",": + self.token() # swallow ',' + self.token() # swallow ']' + self.assertToken(self.token(), ")") + return BoxerDrs(list(refs), conds) + + def _handle_binary_expression(self, make_callback): + self.assertToken(self.token(), "(") + drs1 = self.process_next_expression(None) + self.assertToken(self.token(), ",") + drs2 = self.process_next_expression(None) + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: make_callback( + sent_index, word_indices, drs1, drs2 + ) + + def _handle_alfa(self, make_callback): + self.assertToken(self.token(), "(") + type = self.token() + self.assertToken(self.token(), ",") + drs1 = self.process_next_expression(None) + self.assertToken(self.token(), ",") + drs2 = self.process_next_expression(None) + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: make_callback( + sent_index, word_indices, drs1, drs2 + ) + + def _handle_eq(self): + self.assertToken(self.token(), "(") + var1 = self.parse_variable() + self.assertToken(self.token(), ",") + var2 = self.parse_variable() + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerEq( + self.discourse_id, sent_index, word_indices, var1, var2 + ) + + def _handle_whq(self): + self.assertToken(self.token(), "(") + self.assertToken(self.token(), "[") + ans_types = [] + while self.token(0) != "]": + cat = self.token() + self.assertToken(self.token(), ":") + if cat == "des": + ans_types.append(self.token()) + elif cat == "num": + ans_types.append("number") + typ = self.token() + if typ == "cou": + ans_types.append("count") + else: + ans_types.append(typ) + else: + ans_types.append(self.token()) + self.token() # swallow the ']' + + self.assertToken(self.token(), ",") + d1 = self.process_next_expression(None) + self.assertToken(self.token(), ",") + ref = self.parse_variable() + self.assertToken(self.token(), ",") + d2 = self.process_next_expression(None) + self.assertToken(self.token(), ")") + return lambda sent_index, word_indices: BoxerWhq( + self.discourse_id, sent_index, word_indices, ans_types, d1, ref, d2 + ) + + def _make_merge_expression(self, sent_index, word_indices, drs1, drs2): + return BoxerDrs(drs1.refs + drs2.refs, drs1.conds + drs2.conds) + + def _make_or_expression(self, sent_index, word_indices, drs1, drs2): + return BoxerOr(self.discourse_id, sent_index, word_indices, drs1, drs2) + + def _make_imp_expression(self, sent_index, word_indices, drs1, drs2): + return BoxerDrs(drs1.refs, drs1.conds, drs2) + + def parse_variable(self): + var = self.token() + assert re.match(r"^[exps]\d+$", var), var + return var + + def parse_index(self): + return int(self.token()) + + def _sent_and_word_indices(self, indices): + """ + :return: list of (sent_index, word_indices) tuples + """ + sent_indices = {(i / 1000) - 1 for i in indices if i >= 0} + if sent_indices: + pairs = [] + for sent_index in sent_indices: + word_indices = [ + (i % 1000) - 1 for i in indices if sent_index == (i / 1000) - 1 + ] + pairs.append((sent_index, word_indices)) + return pairs + else: + word_indices = [(i % 1000) - 1 for i in indices] + return [(None, word_indices)] + + +class BoxerDrsParser(DrtParser): + """ + Reparse the str form of subclasses of ``AbstractBoxerDrs`` + """ + + def __init__(self, discourse_id=None): + DrtParser.__init__(self) + self.discourse_id = discourse_id + + def get_all_symbols(self): + return [ + DrtTokens.OPEN, + DrtTokens.CLOSE, + DrtTokens.COMMA, + DrtTokens.OPEN_BRACKET, + DrtTokens.CLOSE_BRACKET, + ] + + def attempt_adjuncts(self, expression, context): + return expression + + def handle(self, tok, context): + try: + # if tok == 'drs': + # self.assertNextToken(DrtTokens.OPEN) + # label = int(self.token()) + # self.assertNextToken(DrtTokens.COMMA) + # refs = list(map(int, self.handle_refs())) + # self.assertNextToken(DrtTokens.COMMA) + # conds = self.handle_conds(None) + # self.assertNextToken(DrtTokens.CLOSE) + # return BoxerDrs(label, refs, conds) + if tok == "pred": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = self.nullableIntToken() + self.assertNextToken(DrtTokens.COMMA) + word_ids = list(map(int, self.handle_refs())) + self.assertNextToken(DrtTokens.COMMA) + variable = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + name = self.token() + self.assertNextToken(DrtTokens.COMMA) + pos = self.token() + self.assertNextToken(DrtTokens.COMMA) + sense = int(self.token()) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerPred(disc_id, sent_id, word_ids, variable, name, pos, sense) + elif tok == "named": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + word_ids = map(int, self.handle_refs()) + self.assertNextToken(DrtTokens.COMMA) + variable = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + name = self.token() + self.assertNextToken(DrtTokens.COMMA) + type = self.token() + self.assertNextToken(DrtTokens.COMMA) + sense = int(self.token()) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerNamed( + disc_id, sent_id, word_ids, variable, name, type, sense + ) + elif tok == "rel": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = self.nullableIntToken() + self.assertNextToken(DrtTokens.COMMA) + word_ids = list(map(int, self.handle_refs())) + self.assertNextToken(DrtTokens.COMMA) + var1 = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + var2 = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + rel = self.token() + self.assertNextToken(DrtTokens.COMMA) + sense = int(self.token()) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerRel(disc_id, sent_id, word_ids, var1, var2, rel, sense) + elif tok == "prop": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + word_ids = list(map(int, self.handle_refs())) + self.assertNextToken(DrtTokens.COMMA) + variable = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + drs = self.process_next_expression(None) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerProp(disc_id, sent_id, word_ids, variable, drs) + elif tok == "not": + self.assertNextToken(DrtTokens.OPEN) + drs = self.process_next_expression(None) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerNot(drs) + elif tok == "imp": + self.assertNextToken(DrtTokens.OPEN) + drs1 = self.process_next_expression(None) + self.assertNextToken(DrtTokens.COMMA) + drs2 = self.process_next_expression(None) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerDrs(drs1.refs, drs1.conds, drs2) + elif tok == "or": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = self.nullableIntToken() + self.assertNextToken(DrtTokens.COMMA) + word_ids = map(int, self.handle_refs()) + self.assertNextToken(DrtTokens.COMMA) + drs1 = self.process_next_expression(None) + self.assertNextToken(DrtTokens.COMMA) + drs2 = self.process_next_expression(None) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerOr(disc_id, sent_id, word_ids, drs1, drs2) + elif tok == "eq": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = self.nullableIntToken() + self.assertNextToken(DrtTokens.COMMA) + word_ids = list(map(int, self.handle_refs())) + self.assertNextToken(DrtTokens.COMMA) + var1 = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + var2 = int(self.token()) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerEq(disc_id, sent_id, word_ids, var1, var2) + elif tok == "card": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = self.nullableIntToken() + self.assertNextToken(DrtTokens.COMMA) + word_ids = map(int, self.handle_refs()) + self.assertNextToken(DrtTokens.COMMA) + var = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + value = self.token() + self.assertNextToken(DrtTokens.COMMA) + type = self.token() + self.assertNextToken(DrtTokens.CLOSE) + return BoxerCard(disc_id, sent_id, word_ids, var, value, type) + elif tok == "whq": + self.assertNextToken(DrtTokens.OPEN) + disc_id = ( + self.discourse_id if self.discourse_id is not None else self.token() + ) + self.assertNextToken(DrtTokens.COMMA) + sent_id = self.nullableIntToken() + self.assertNextToken(DrtTokens.COMMA) + word_ids = list(map(int, self.handle_refs())) + self.assertNextToken(DrtTokens.COMMA) + ans_types = self.handle_refs() + self.assertNextToken(DrtTokens.COMMA) + drs1 = self.process_next_expression(None) + self.assertNextToken(DrtTokens.COMMA) + var = int(self.token()) + self.assertNextToken(DrtTokens.COMMA) + drs2 = self.process_next_expression(None) + self.assertNextToken(DrtTokens.CLOSE) + return BoxerWhq(disc_id, sent_id, word_ids, ans_types, drs1, var, drs2) + except Exception as e: + raise LogicalExpressionException(self._currentIndex, str(e)) from e + assert False, repr(tok) + + def nullableIntToken(self): + t = self.token() + return int(t) if t != "None" else None + + def get_next_token_variable(self, description): + try: + return self.token() + except ExpectedMoreTokensException as e: + raise ExpectedMoreTokensException(e.index, "Variable expected.") from e + + +class AbstractBoxerDrs: + def variables(self): + """ + :return: (set, set, set) + """ + variables, events, propositions = self._variables() + return (variables - (events | propositions), events, propositions - events) + + def variable_types(self): + vartypes = {} + for t, vars in zip(("z", "e", "p"), self.variables()): + for v in vars: + vartypes[v] = t + return vartypes + + def _variables(self): + """ + :return: (set, set, set) + """ + return (set(), set(), set()) + + def atoms(self): + return set() + + def clean(self): + return self + + def _clean_name(self, name): + return name.replace("-", "_").replace("'", "_") + + def renumber_sentences(self, f): + return self + + def __hash__(self): + return hash(f"{self}") + + +class BoxerDrs(AbstractBoxerDrs): + def __init__(self, refs, conds, consequent=None): + AbstractBoxerDrs.__init__(self) + self.refs = refs + self.conds = conds + self.consequent = consequent + + def _variables(self): + variables = (set(), set(), set()) + for cond in self.conds: + for s, v in zip(variables, cond._variables()): + s.update(v) + if self.consequent is not None: + for s, v in zip(variables, self.consequent._variables()): + s.update(v) + return variables + + def atoms(self): + atoms = reduce(operator.or_, (cond.atoms() for cond in self.conds), set()) + if self.consequent is not None: + atoms.update(self.consequent.atoms()) + return atoms + + def clean(self): + consequent = self.consequent.clean() if self.consequent else None + return BoxerDrs(self.refs, [c.clean() for c in self.conds], consequent) + + def renumber_sentences(self, f): + consequent = self.consequent.renumber_sentences(f) if self.consequent else None + return BoxerDrs( + self.refs, [c.renumber_sentences(f) for c in self.conds], consequent + ) + + def __repr__(self): + s = "drs([{}], [{}])".format( + ", ".join("%s" % r for r in self.refs), + ", ".join("%s" % c for c in self.conds), + ) + if self.consequent is not None: + s = f"imp({s}, {self.consequent})" + return s + + def __eq__(self, other): + return ( + self.__class__ == other.__class__ + and self.refs == other.refs + and len(self.conds) == len(other.conds) + and reduce( + operator.and_, (c1 == c2 for c1, c2 in zip(self.conds, other.conds)) + ) + and self.consequent == other.consequent + ) + + def __ne__(self, other): + return not self == other + + __hash__ = AbstractBoxerDrs.__hash__ + + +class BoxerNot(AbstractBoxerDrs): + def __init__(self, drs): + AbstractBoxerDrs.__init__(self) + self.drs = drs + + def _variables(self): + return self.drs._variables() + + def atoms(self): + return self.drs.atoms() + + def clean(self): + return BoxerNot(self.drs.clean()) + + def renumber_sentences(self, f): + return BoxerNot(self.drs.renumber_sentences(f)) + + def __repr__(self): + return "not(%s)" % (self.drs) + + def __eq__(self, other): + return self.__class__ == other.__class__ and self.drs == other.drs + + def __ne__(self, other): + return not self == other + + __hash__ = AbstractBoxerDrs.__hash__ + + +class BoxerIndexed(AbstractBoxerDrs): + def __init__(self, discourse_id, sent_index, word_indices): + AbstractBoxerDrs.__init__(self) + self.discourse_id = discourse_id + self.sent_index = sent_index + self.word_indices = word_indices + + def atoms(self): + return {self} + + def __eq__(self, other): + return ( + self.__class__ == other.__class__ + and self.discourse_id == other.discourse_id + and self.sent_index == other.sent_index + and self.word_indices == other.word_indices + and reduce(operator.and_, (s == o for s, o in zip(self, other))) + ) + + def __ne__(self, other): + return not self == other + + __hash__ = AbstractBoxerDrs.__hash__ + + def __repr__(self): + s = "{}({}, {}, [{}]".format( + self._pred(), + self.discourse_id, + self.sent_index, + ", ".join("%s" % wi for wi in self.word_indices), + ) + for v in self: + s += ", %s" % v + return s + ")" + + +class BoxerPred(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, var, name, pos, sense): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.var = var + self.name = name + self.pos = pos + self.sense = sense + + def _variables(self): + return ({self.var}, set(), set()) + + def change_var(self, var): + return BoxerPred( + self.discourse_id, + self.sent_index, + self.word_indices, + var, + self.name, + self.pos, + self.sense, + ) + + def clean(self): + return BoxerPred( + self.discourse_id, + self.sent_index, + self.word_indices, + self.var, + self._clean_name(self.name), + self.pos, + self.sense, + ) + + def renumber_sentences(self, f): + new_sent_index = f(self.sent_index) + return BoxerPred( + self.discourse_id, + new_sent_index, + self.word_indices, + self.var, + self.name, + self.pos, + self.sense, + ) + + def __iter__(self): + return iter((self.var, self.name, self.pos, self.sense)) + + def _pred(self): + return "pred" + + +class BoxerNamed(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, var, name, type, sense): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.var = var + self.name = name + self.type = type + self.sense = sense + + def _variables(self): + return ({self.var}, set(), set()) + + def change_var(self, var): + return BoxerNamed( + self.discourse_id, + self.sent_index, + self.word_indices, + var, + self.name, + self.type, + self.sense, + ) + + def clean(self): + return BoxerNamed( + self.discourse_id, + self.sent_index, + self.word_indices, + self.var, + self._clean_name(self.name), + self.type, + self.sense, + ) + + def renumber_sentences(self, f): + return BoxerNamed( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.var, + self.name, + self.type, + self.sense, + ) + + def __iter__(self): + return iter((self.var, self.name, self.type, self.sense)) + + def _pred(self): + return "named" + + +class BoxerRel(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, var1, var2, rel, sense): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.var1 = var1 + self.var2 = var2 + self.rel = rel + self.sense = sense + + def _variables(self): + return ({self.var1, self.var2}, set(), set()) + + def clean(self): + return BoxerRel( + self.discourse_id, + self.sent_index, + self.word_indices, + self.var1, + self.var2, + self._clean_name(self.rel), + self.sense, + ) + + def renumber_sentences(self, f): + return BoxerRel( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.var1, + self.var2, + self.rel, + self.sense, + ) + + def __iter__(self): + return iter((self.var1, self.var2, self.rel, self.sense)) + + def _pred(self): + return "rel" + + +class BoxerProp(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, var, drs): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.var = var + self.drs = drs + + def _variables(self): + return tuple( + map(operator.or_, (set(), set(), {self.var}), self.drs._variables()) + ) + + def referenced_labels(self): + return {self.drs} + + def atoms(self): + return self.drs.atoms() + + def clean(self): + return BoxerProp( + self.discourse_id, + self.sent_index, + self.word_indices, + self.var, + self.drs.clean(), + ) + + def renumber_sentences(self, f): + return BoxerProp( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.var, + self.drs.renumber_sentences(f), + ) + + def __iter__(self): + return iter((self.var, self.drs)) + + def _pred(self): + return "prop" + + +class BoxerEq(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, var1, var2): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.var1 = var1 + self.var2 = var2 + + def _variables(self): + return ({self.var1, self.var2}, set(), set()) + + def atoms(self): + return set() + + def renumber_sentences(self, f): + return BoxerEq( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.var1, + self.var2, + ) + + def __iter__(self): + return iter((self.var1, self.var2)) + + def _pred(self): + return "eq" + + +class BoxerCard(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, var, value, type): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.var = var + self.value = value + self.type = type + + def _variables(self): + return ({self.var}, set(), set()) + + def renumber_sentences(self, f): + return BoxerCard( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.var, + self.value, + self.type, + ) + + def __iter__(self): + return iter((self.var, self.value, self.type)) + + def _pred(self): + return "card" + + +class BoxerOr(BoxerIndexed): + def __init__(self, discourse_id, sent_index, word_indices, drs1, drs2): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.drs1 = drs1 + self.drs2 = drs2 + + def _variables(self): + return tuple(map(operator.or_, self.drs1._variables(), self.drs2._variables())) + + def atoms(self): + return self.drs1.atoms() | self.drs2.atoms() + + def clean(self): + return BoxerOr( + self.discourse_id, + self.sent_index, + self.word_indices, + self.drs1.clean(), + self.drs2.clean(), + ) + + def renumber_sentences(self, f): + return BoxerOr( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.drs1, + self.drs2, + ) + + def __iter__(self): + return iter((self.drs1, self.drs2)) + + def _pred(self): + return "or" + + +class BoxerWhq(BoxerIndexed): + def __init__( + self, discourse_id, sent_index, word_indices, ans_types, drs1, variable, drs2 + ): + BoxerIndexed.__init__(self, discourse_id, sent_index, word_indices) + self.ans_types = ans_types + self.drs1 = drs1 + self.variable = variable + self.drs2 = drs2 + + def _variables(self): + return tuple( + map( + operator.or_, + ({self.variable}, set(), set()), + self.drs1._variables(), + self.drs2._variables(), + ) + ) + + def atoms(self): + return self.drs1.atoms() | self.drs2.atoms() + + def clean(self): + return BoxerWhq( + self.discourse_id, + self.sent_index, + self.word_indices, + self.ans_types, + self.drs1.clean(), + self.variable, + self.drs2.clean(), + ) + + def renumber_sentences(self, f): + return BoxerWhq( + self.discourse_id, + f(self.sent_index), + self.word_indices, + self.ans_types, + self.drs1, + self.variable, + self.drs2, + ) + + def __iter__(self): + return iter( + ("[" + ",".join(self.ans_types) + "]", self.drs1, self.variable, self.drs2) + ) + + def _pred(self): + return "whq" + + +class PassthroughBoxerDrsInterpreter: + def interpret(self, ex): + return ex + + +class NltkDrtBoxerDrsInterpreter: + def __init__(self, occur_index=False): + self._occur_index = occur_index + + def interpret(self, ex): + """ + :param ex: ``AbstractBoxerDrs`` + :return: ``DrtExpression`` + """ + if isinstance(ex, BoxerDrs): + drs = DRS( + [Variable(r) for r in ex.refs], list(map(self.interpret, ex.conds)) + ) + if ex.consequent is not None: + drs.consequent = self.interpret(ex.consequent) + return drs + elif isinstance(ex, BoxerNot): + return DrtNegatedExpression(self.interpret(ex.drs)) + elif isinstance(ex, BoxerPred): + pred = self._add_occur_indexing(f"{ex.pos}_{ex.name}", ex) + return self._make_atom(pred, ex.var) + elif isinstance(ex, BoxerNamed): + pred = self._add_occur_indexing(f"ne_{ex.type}_{ex.name}", ex) + return self._make_atom(pred, ex.var) + elif isinstance(ex, BoxerRel): + pred = self._add_occur_indexing("%s" % (ex.rel), ex) + return self._make_atom(pred, ex.var1, ex.var2) + elif isinstance(ex, BoxerProp): + return DrtProposition(Variable(ex.var), self.interpret(ex.drs)) + elif isinstance(ex, BoxerEq): + return DrtEqualityExpression( + DrtVariableExpression(Variable(ex.var1)), + DrtVariableExpression(Variable(ex.var2)), + ) + elif isinstance(ex, BoxerCard): + pred = self._add_occur_indexing(f"card_{ex.type}_{ex.value}", ex) + return self._make_atom(pred, ex.var) + elif isinstance(ex, BoxerOr): + return DrtOrExpression(self.interpret(ex.drs1), self.interpret(ex.drs2)) + elif isinstance(ex, BoxerWhq): + drs1 = self.interpret(ex.drs1) + drs2 = self.interpret(ex.drs2) + return DRS(drs1.refs + drs2.refs, drs1.conds + drs2.conds) + assert False, f"{ex.__class__.__name__}: {ex}" + + def _make_atom(self, pred, *args): + accum = DrtVariableExpression(Variable(pred)) + for arg in args: + accum = DrtApplicationExpression( + accum, DrtVariableExpression(Variable(arg)) + ) + return accum + + def _add_occur_indexing(self, base, ex): + if self._occur_index and ex.sent_index is not None: + if ex.discourse_id: + base += "_%s" % ex.discourse_id + base += "_s%s" % ex.sent_index + base += "_w%s" % sorted(ex.word_indices)[0] + return base + + +class UnparseableInputException(Exception): + pass + + +if __name__ == "__main__": + opts = OptionParser("usage: %prog TEXT [options]") + opts.add_option( + "--verbose", + "-v", + help="display verbose logs", + action="store_true", + default=False, + dest="verbose", + ) + opts.add_option( + "--fol", "-f", help="output FOL", action="store_true", default=False, dest="fol" + ) + opts.add_option( + "--question", + "-q", + help="input is a question", + action="store_true", + default=False, + dest="question", + ) + opts.add_option( + "--occur", + "-o", + help="occurrence index", + action="store_true", + default=False, + dest="occur_index", + ) + (options, args) = opts.parse_args() + + if len(args) != 1: + opts.error("incorrect number of arguments") + + interpreter = NltkDrtBoxerDrsInterpreter(occur_index=options.occur_index) + drs = Boxer(interpreter).interpret_multi( + args[0].split(r"\n"), question=options.question, verbose=options.verbose + ) + if drs is None: + print(None) + else: + drs = drs.simplify().eliminate_equality() + if options.fol: + print(drs.fol().normalize()) + else: + drs.pretty_print() diff --git a/nltk/sem/chat80.py b/nltk/sem/chat80.py new file mode 100644 index 0000000..1248e0f --- /dev/null +++ b/nltk/sem/chat80.py @@ -0,0 +1,857 @@ +# Natural Language Toolkit: Chat-80 KB Reader +# See https://www.w3.org/TR/swbp-skos-core-guide/ +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein , +# URL: +# For license information, see LICENSE.TXT + +r""" +Overview +======== + +Chat-80 was a natural language system which allowed the user to +interrogate a Prolog knowledge base in the domain of world +geography. It was developed in the early '80s by Warren and Pereira; see +``https://www.aclweb.org/anthology/J82-3002.pdf`` for a description and +``http://www.cis.upenn.edu/~pereira/oldies.html`` for the source +files. + +This module contains functions to extract data from the Chat-80 +relation files ('the world database'), and convert then into a format +that can be incorporated in the FOL models of +``nltk.sem.evaluate``. The code assumes that the Prolog +input files are available in the NLTK corpora directory. + +The Chat-80 World Database consists of the following files:: + + world0.pl + rivers.pl + cities.pl + countries.pl + contain.pl + borders.pl + +This module uses a slightly modified version of ``world0.pl``, in which +a set of Prolog rules have been omitted. The modified file is named +``world1.pl``. Currently, the file ``rivers.pl`` is not read in, since +it uses a list rather than a string in the second field. + +Reading Chat-80 Files +===================== + +Chat-80 relations are like tables in a relational database. The +relation acts as the name of the table; the first argument acts as the +'primary key'; and subsequent arguments are further fields in the +table. In general, the name of the table provides a label for a unary +predicate whose extension is all the primary keys. For example, +relations in ``cities.pl`` are of the following form:: + + 'city(athens,greece,1368).' + +Here, ``'athens'`` is the key, and will be mapped to a member of the +unary predicate *city*. + +The fields in the table are mapped to binary predicates. The first +argument of the predicate is the primary key, while the second +argument is the data in the relevant field. Thus, in the above +example, the third field is mapped to the binary predicate +*population_of*, whose extension is a set of pairs such as +``'(athens, 1368)'``. + +An exception to this general framework is required by the relations in +the files ``borders.pl`` and ``contains.pl``. These contain facts of the +following form:: + + 'borders(albania,greece).' + + 'contains0(africa,central_africa).' + +We do not want to form a unary concept out the element in +the first field of these records, and we want the label of the binary +relation just to be ``'border'``/``'contain'`` respectively. + +In order to drive the extraction process, we use 'relation metadata bundles' +which are Python dictionaries such as the following:: + + city = {'label': 'city', + 'closures': [], + 'schema': ['city', 'country', 'population'], + 'filename': 'cities.pl'} + +According to this, the file ``city['filename']`` contains a list of +relational tuples (or more accurately, the corresponding strings in +Prolog form) whose predicate symbol is ``city['label']`` and whose +relational schema is ``city['schema']``. The notion of a ``closure`` is +discussed in the next section. + +Concepts +======== +In order to encapsulate the results of the extraction, a class of +``Concept`` objects is introduced. A ``Concept`` object has a number of +attributes, in particular a ``prefLabel`` and ``extension``, which make +it easier to inspect the output of the extraction. In addition, the +``extension`` can be further processed: in the case of the ``'border'`` +relation, we check that the relation is symmetric, and in the case +of the ``'contain'`` relation, we carry out the transitive +closure. The closure properties associated with a concept is +indicated in the relation metadata, as indicated earlier. + +The ``extension`` of a ``Concept`` object is then incorporated into a +``Valuation`` object. + +Persistence +=========== +The functions ``val_dump`` and ``val_load`` are provided to allow a +valuation to be stored in a persistent database and re-loaded, rather +than having to be re-computed each time. + +Individuals and Lexical Items +============================= +As well as deriving relations from the Chat-80 data, we also create a +set of individual constants, one for each entity in the domain. The +individual constants are string-identical to the entities. For +example, given a data item such as ``'zloty'``, we add to the valuation +a pair ``('zloty', 'zloty')``. In order to parse English sentences that +refer to these entities, we also create a lexical item such as the +following for each individual constant:: + + PropN[num=sg, sem=<\P.(P zloty)>] -> 'Zloty' + +The set of rules is written to the file ``chat_pnames.cfg`` in the +current directory. + +""" + +import os +import re +import shelve +import sys + +import nltk.data + +########################################################################### +# Chat-80 relation metadata bundles needed to build the valuation +########################################################################### + +borders = { + "rel_name": "borders", + "closures": ["symmetric"], + "schema": ["region", "border"], + "filename": "borders.pl", +} + +contains = { + "rel_name": "contains0", + "closures": ["transitive"], + "schema": ["region", "contain"], + "filename": "contain.pl", +} + +city = { + "rel_name": "city", + "closures": [], + "schema": ["city", "country", "population"], + "filename": "cities.pl", +} + +country = { + "rel_name": "country", + "closures": [], + "schema": [ + "country", + "region", + "latitude", + "longitude", + "area", + "population", + "capital", + "currency", + ], + "filename": "countries.pl", +} + +circle_of_lat = { + "rel_name": "circle_of_latitude", + "closures": [], + "schema": ["circle_of_latitude", "degrees"], + "filename": "world1.pl", +} + +circle_of_long = { + "rel_name": "circle_of_longitude", + "closures": [], + "schema": ["circle_of_longitude", "degrees"], + "filename": "world1.pl", +} + +continent = { + "rel_name": "continent", + "closures": [], + "schema": ["continent"], + "filename": "world1.pl", +} + +region = { + "rel_name": "in_continent", + "closures": [], + "schema": ["region", "continent"], + "filename": "world1.pl", +} + +ocean = { + "rel_name": "ocean", + "closures": [], + "schema": ["ocean"], + "filename": "world1.pl", +} + +sea = {"rel_name": "sea", "closures": [], "schema": ["sea"], "filename": "world1.pl"} + + +items = [ + "borders", + "contains", + "city", + "country", + "circle_of_lat", + "circle_of_long", + "continent", + "region", + "ocean", + "sea", +] +items = tuple(sorted(items)) + +item_metadata = { + "borders": borders, + "contains": contains, + "city": city, + "country": country, + "circle_of_lat": circle_of_lat, + "circle_of_long": circle_of_long, + "continent": continent, + "region": region, + "ocean": ocean, + "sea": sea, +} + +rels = item_metadata.values() + +not_unary = ["borders.pl", "contain.pl"] + +########################################################################### + + +class Concept: + """ + A Concept class, loosely based on SKOS + (https://www.w3.org/TR/swbp-skos-core-guide/). + """ + + def __init__(self, prefLabel, arity, altLabels=[], closures=[], extension=set()): + """ + :param prefLabel: the preferred label for the concept + :type prefLabel: str + :param arity: the arity of the concept + :type arity: int + :param altLabels: other (related) labels + :type altLabels: list + :param closures: closure properties of the extension + (list items can be ``symmetric``, ``reflexive``, ``transitive``) + :type closures: list + :param extension: the extensional value of the concept + :type extension: set + """ + self.prefLabel = prefLabel + self.arity = arity + self.altLabels = altLabels + self.closures = closures + # keep _extension internally as a set + self._extension = extension + # public access is via a list (for slicing) + self.extension = sorted(list(extension)) + + def __str__(self): + # _extension = '' + # for element in sorted(self.extension): + # if isinstance(element, tuple): + # element = '(%s, %s)' % (element) + # _extension += element + ', ' + # _extension = _extension[:-1] + + return "Label = '{}'\nArity = {}\nExtension = {}".format( + self.prefLabel, + self.arity, + self.extension, + ) + + def __repr__(self): + return "Concept('%s')" % self.prefLabel + + def augment(self, data): + """ + Add more data to the ``Concept``'s extension set. + + :param data: a new semantic value + :type data: string or pair of strings + :rtype: set + + """ + self._extension.add(data) + self.extension = sorted(list(self._extension)) + return self._extension + + def _make_graph(self, s): + """ + Convert a set of pairs into an adjacency linked list encoding of a graph. + """ + g = {} + for x, y in s: + if x in g: + g[x].append(y) + else: + g[x] = [y] + return g + + def _transclose(self, g): + """ + Compute the transitive closure of a graph represented as a linked list. + """ + for x in g: + for adjacent in g[x]: + # check that adjacent is a key + if adjacent in g: + for y in g[adjacent]: + if y not in g[x]: + g[x].append(y) + return g + + def _make_pairs(self, g): + """ + Convert an adjacency linked list back into a set of pairs. + """ + pairs = [] + for node in g: + for adjacent in g[node]: + pairs.append((node, adjacent)) + return set(pairs) + + def close(self): + """ + Close a binary relation in the ``Concept``'s extension set. + + :return: a new extension for the ``Concept`` in which the + relation is closed under a given property + """ + from nltk.sem import is_rel + + assert is_rel(self._extension) + if "symmetric" in self.closures: + pairs = [] + for x, y in self._extension: + pairs.append((y, x)) + sym = set(pairs) + self._extension = self._extension.union(sym) + if "transitive" in self.closures: + all = self._make_graph(self._extension) + closed = self._transclose(all) + trans = self._make_pairs(closed) + self._extension = self._extension.union(trans) + self.extension = sorted(list(self._extension)) + + +def clause2concepts(filename, rel_name, schema, closures=[]): + """ + Convert a file of Prolog clauses into a list of ``Concept`` objects. + + :param filename: filename containing the relations + :type filename: str + :param rel_name: name of the relation + :type rel_name: str + :param schema: the schema used in a set of relational tuples + :type schema: list + :param closures: closure properties for the extension of the concept + :type closures: list + :return: a list of ``Concept`` objects + :rtype: list + """ + concepts = [] + # position of the subject of a binary relation + subj = 0 + # label of the 'primary key' + pkey = schema[0] + # fields other than the primary key + fields = schema[1:] + + # convert a file into a list of lists + records = _str2records(filename, rel_name) + + # add a unary concept corresponding to the set of entities + # in the primary key position + # relations in 'not_unary' are more like ordinary binary relations + if filename not in not_unary: + concepts.append(unary_concept(pkey, subj, records)) + + # add a binary concept for each non-key field + for field in fields: + obj = schema.index(field) + concepts.append(binary_concept(field, closures, subj, obj, records)) + + return concepts + + +def cities2table(filename, rel_name, dbname, verbose=False, setup=False): + """ + Convert a file of Prolog clauses into a database table. + + This is not generic, since it doesn't allow arbitrary + schemas to be set as a parameter. + + Intended usage:: + + cities2table('cities.pl', 'city', 'city.db', verbose=True, setup=True) + + :param filename: filename containing the relations + :type filename: str + :param rel_name: name of the relation + :type rel_name: str + :param dbname: filename of persistent store + :type schema: str + """ + import sqlite3 + + records = _str2records(filename, rel_name) + connection = sqlite3.connect(dbname) + cur = connection.cursor() + if setup: + cur.execute( + """CREATE TABLE city_table + (City text, Country text, Population int)""" + ) + + table_name = "city_table" + for t in records: + cur.execute("insert into %s values (?,?,?)" % table_name, t) + if verbose: + print("inserting values into %s: " % table_name, t) + connection.commit() + if verbose: + print("Committing update to %s" % dbname) + cur.close() + + +def sql_query(dbname, query): + """ + Execute an SQL query over a database. + :param dbname: filename of persistent store + :type schema: str + :param query: SQL query + :type rel_name: str + """ + import sqlite3 + + try: + path = nltk.data.find(dbname) + connection = sqlite3.connect(str(path)) + cur = connection.cursor() + return cur.execute(query) + except (ValueError, sqlite3.OperationalError): + import warnings + + warnings.warn( + "Make sure the database file %s is installed and uncompressed." % dbname + ) + raise + + +def _str2records(filename, rel): + """ + Read a file into memory and convert each relation clause into a list. + """ + recs = [] + contents = nltk.data.load("corpora/chat80/%s" % filename, format="text") + for line in contents.splitlines(): + if line.startswith(rel): + line = re.sub(rel + r"\(", "", line) + line = re.sub(r"\)\.$", "", line) + record = line.split(",") + recs.append(record) + return recs + + +def unary_concept(label, subj, records): + """ + Make a unary concept out of the primary key in a record. + + A record is a list of entities in some relation, such as + ``['france', 'paris']``, where ``'france'`` is acting as the primary + key. + + :param label: the preferred label for the concept + :type label: string + :param subj: position in the record of the subject of the predicate + :type subj: int + :param records: a list of records + :type records: list of lists + :return: ``Concept`` of arity 1 + :rtype: Concept + """ + c = Concept(label, arity=1, extension=set()) + for record in records: + c.augment(record[subj]) + return c + + +def binary_concept(label, closures, subj, obj, records): + """ + Make a binary concept out of the primary key and another field in a record. + + A record is a list of entities in some relation, such as + ``['france', 'paris']``, where ``'france'`` is acting as the primary + key, and ``'paris'`` stands in the ``'capital_of'`` relation to + ``'france'``. + + More generally, given a record such as ``['a', 'b', 'c']``, where + label is bound to ``'B'``, and ``obj`` bound to 1, the derived + binary concept will have label ``'B_of'``, and its extension will + be a set of pairs such as ``('a', 'b')``. + + + :param label: the base part of the preferred label for the concept + :type label: str + :param closures: closure properties for the extension of the concept + :type closures: list + :param subj: position in the record of the subject of the predicate + :type subj: int + :param obj: position in the record of the object of the predicate + :type obj: int + :param records: a list of records + :type records: list of lists + :return: ``Concept`` of arity 2 + :rtype: Concept + """ + if not label == "border" and not label == "contain": + label = label + "_of" + c = Concept(label, arity=2, closures=closures, extension=set()) + for record in records: + c.augment((record[subj], record[obj])) + # close the concept's extension according to the properties in closures + c.close() + return c + + +def process_bundle(rels): + """ + Given a list of relation metadata bundles, make a corresponding + dictionary of concepts, indexed by the relation name. + + :param rels: bundle of metadata needed for constructing a concept + :type rels: list(dict) + :return: a dictionary of concepts, indexed by the relation name. + :rtype: dict(str): Concept + """ + concepts = {} + for rel in rels: + rel_name = rel["rel_name"] + closures = rel["closures"] + schema = rel["schema"] + filename = rel["filename"] + + concept_list = clause2concepts(filename, rel_name, schema, closures) + for c in concept_list: + label = c.prefLabel + if label in concepts: + for data in c.extension: + concepts[label].augment(data) + concepts[label].close() + else: + concepts[label] = c + return concepts + + +def make_valuation(concepts, read=False, lexicon=False): + """ + Convert a list of ``Concept`` objects into a list of (label, extension) pairs; + optionally create a ``Valuation`` object. + + :param concepts: concepts + :type concepts: list(Concept) + :param read: if ``True``, ``(symbol, set)`` pairs are read into a ``Valuation`` + :type read: bool + :rtype: list or Valuation + """ + vals = [] + + for c in concepts: + vals.append((c.prefLabel, c.extension)) + if lexicon: + read = True + if read: + from nltk.sem import Valuation + + val = Valuation({}) + val.update(vals) + # add labels for individuals + val = label_indivs(val, lexicon=lexicon) + return val + else: + return vals + + +def val_dump(rels, db): + """ + Make a ``Valuation`` from a list of relation metadata bundles and dump to + persistent database. + + :param rels: bundle of metadata needed for constructing a concept + :type rels: list of dict + :param db: name of file to which data is written. + The suffix '.db' will be automatically appended. + :type db: str + """ + concepts = process_bundle(rels).values() + valuation = make_valuation(concepts, read=True) + db_out = shelve.open(db, "n") + + db_out.update(valuation) + + db_out.close() + + +def val_load(db): + """ + Load a ``Valuation`` from a persistent database. + + :param db: name of file from which data is read. + The suffix '.db' should be omitted from the name. + :type db: str + """ + dbname = db + ".db" + + if not os.access(dbname, os.R_OK): + sys.exit("Cannot read file: %s" % dbname) + else: + db_in = shelve.open(db) + from nltk.sem import Valuation + + val = Valuation(db_in) + # val.read(db_in.items()) + return val + + +# def alpha(str): +# """ +# Utility to filter out non-alphabetic constants. + +#:param str: candidate constant +#:type str: string +#:rtype: bool +# """ +# try: +# int(str) +# return False +# except ValueError: +## some unknown values in records are labeled '?' +# if not str == '?': +# return True + + +def label_indivs(valuation, lexicon=False): + """ + Assign individual constants to the individuals in the domain of a ``Valuation``. + + Given a valuation with an entry of the form ``{'rel': {'a': True}}``, + add a new entry ``{'a': 'a'}``. + + :type valuation: Valuation + :rtype: Valuation + """ + # collect all the individuals into a domain + domain = valuation.domain + # convert the domain into a sorted list of alphabetic terms + # use the same string as a label + pairs = [(e, e) for e in domain] + if lexicon: + lex = make_lex(domain) + with open("chat_pnames.cfg", "w") as outfile: + outfile.writelines(lex) + # read the pairs into the valuation + valuation.update(pairs) + return valuation + + +def make_lex(symbols): + """ + Create lexical CFG rules for each individual symbol. + + Given a valuation with an entry of the form ``{'zloty': 'zloty'}``, + create a lexical rule for the proper name 'Zloty'. + + :param symbols: a list of individual constants in the semantic representation + :type symbols: sequence -- set(str) + :rtype: list(str) + """ + lex = [] + header = """ +################################################################## +# Lexical rules automatically generated by running 'chat80.py -x'. +################################################################## + +""" + lex.append(header) + template = r"PropN[num=sg, sem=<\P.(P %s)>] -> '%s'\n" + + for s in symbols: + parts = s.split("_") + caps = [p.capitalize() for p in parts] + pname = "_".join(caps) + rule = template % (s, pname) + lex.append(rule) + return lex + + +########################################################################### +# Interface function to emulate other corpus readers +########################################################################### + + +def concepts(items=items): + """ + Build a list of concepts corresponding to the relation names in ``items``. + + :param items: names of the Chat-80 relations to extract + :type items: list(str) + :return: the ``Concept`` objects which are extracted from the relations + :rtype: list(Concept) + """ + if isinstance(items, str): + items = (items,) + + rels = [item_metadata[r] for r in items] + + concept_map = process_bundle(rels) + return concept_map.values() + + +########################################################################### + + +def main(): + import sys + from optparse import OptionParser + + description = """ +Extract data from the Chat-80 Prolog files and convert them into a +Valuation object for use in the NLTK semantics package. + """ + + opts = OptionParser(description=description) + opts.set_defaults(verbose=True, lex=False, vocab=False) + opts.add_option( + "-s", "--store", dest="outdb", help="store a valuation in DB", metavar="DB" + ) + opts.add_option( + "-l", + "--load", + dest="indb", + help="load a stored valuation from DB", + metavar="DB", + ) + opts.add_option( + "-c", + "--concepts", + action="store_true", + help="print concepts instead of a valuation", + ) + opts.add_option( + "-r", + "--relation", + dest="label", + help="print concept with label REL (check possible labels with '-v' option)", + metavar="REL", + ) + opts.add_option( + "-q", + "--quiet", + action="store_false", + dest="verbose", + help="don't print out progress info", + ) + opts.add_option( + "-x", + "--lex", + action="store_true", + dest="lex", + help="write a file of lexical entries for country names, then exit", + ) + opts.add_option( + "-v", + "--vocab", + action="store_true", + dest="vocab", + help="print out the vocabulary of concept labels and their arity, then exit", + ) + + (options, args) = opts.parse_args() + if options.outdb and options.indb: + opts.error("Options --store and --load are mutually exclusive") + + if options.outdb: + # write the valuation to a persistent database + if options.verbose: + outdb = options.outdb + ".db" + print("Dumping a valuation to %s" % outdb) + val_dump(rels, options.outdb) + sys.exit(0) + else: + # try to read in a valuation from a database + if options.indb is not None: + dbname = options.indb + ".db" + if not os.access(dbname, os.R_OK): + sys.exit("Cannot read file: %s" % dbname) + else: + valuation = val_load(options.indb) + # we need to create the valuation from scratch + else: + # build some concepts + concept_map = process_bundle(rels) + concepts = concept_map.values() + # just print out the vocabulary + if options.vocab: + items = sorted((c.arity, c.prefLabel) for c in concepts) + for arity, label in items: + print(label, arity) + sys.exit(0) + # show all the concepts + if options.concepts: + for c in concepts: + print(c) + print() + if options.label: + print(concept_map[options.label]) + sys.exit(0) + else: + # turn the concepts into a Valuation + if options.lex: + if options.verbose: + print("Writing out lexical rules") + make_valuation(concepts, lexicon=True) + else: + valuation = make_valuation(concepts, read=True) + print(valuation) + + +def sql_demo(): + """ + Print out every row from the 'city.db' database. + """ + print() + print("Using SQL to extract rows from 'city.db' RDB.") + for row in sql_query("corpora/city_database/city.db", "SELECT * FROM city_table"): + print(row) + + +if __name__ == "__main__": + main() + sql_demo() diff --git a/nltk/sem/cooper_storage.py b/nltk/sem/cooper_storage.py new file mode 100644 index 0000000..b5a79ae --- /dev/null +++ b/nltk/sem/cooper_storage.py @@ -0,0 +1,124 @@ +# Natural Language Toolkit: Cooper storage for Quantifier Ambiguity +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +from nltk.parse import load_parser +from nltk.parse.featurechart import InstantiateVarsChart +from nltk.sem.logic import ApplicationExpression, LambdaExpression, Variable + + +class CooperStore: + """ + A container for handling quantifier ambiguity via Cooper storage. + """ + + def __init__(self, featstruct): + """ + :param featstruct: The value of the ``sem`` node in a tree from + ``parse_with_bindops()`` + :type featstruct: FeatStruct (with features ``core`` and ``store``) + + """ + self.featstruct = featstruct + self.readings = [] + try: + self.core = featstruct["CORE"] + self.store = featstruct["STORE"] + except KeyError: + print("%s is not a Cooper storage structure" % featstruct) + + def _permute(self, lst): + """ + :return: An iterator over the permutations of the input list + :type lst: list + :rtype: iter + """ + remove = lambda lst0, index: lst0[:index] + lst0[index + 1 :] + if lst: + for index, x in enumerate(lst): + for y in self._permute(remove(lst, index)): + yield (x,) + y + else: + yield () + + def s_retrieve(self, trace=False): + r""" + Carry out S-Retrieval of binding operators in store. If hack=True, + serialize the bindop and core as strings and reparse. Ugh. + + Each permutation of the store (i.e. list of binding operators) is + taken to be a possible scoping of quantifiers. We iterate through the + binding operators in each permutation, and successively apply them to + the current term, starting with the core semantic representation, + working from the inside out. + + Binding operators are of the form:: + + bo(\P.all x.(man(x) -> P(x)),z1) + """ + for perm, store_perm in enumerate(self._permute(self.store)): + if trace: + print("Permutation %s" % (perm + 1)) + term = self.core + for bindop in store_perm: + # we just want the arguments that are wrapped by the 'bo' predicate + quant, varex = tuple(bindop.args) + # use var to make an abstraction over the current term and then + # apply the quantifier to it + term = ApplicationExpression( + quant, LambdaExpression(varex.variable, term) + ) + if trace: + print(" ", term) + term = term.simplify() + self.readings.append(term) + + +def parse_with_bindops(sentence, grammar=None, trace=0): + """ + Use a grammar with Binding Operators to parse a sentence. + """ + if not grammar: + grammar = "grammars/book_grammars/storage.fcfg" + parser = load_parser(grammar, trace=trace, chart_class=InstantiateVarsChart) + # Parse the sentence. + tokens = sentence.split() + return list(parser.parse(tokens)) + + +def demo(): + from nltk.sem import cooper_storage as cs + + sentence = "every girl chases a dog" + # sentence = "a man gives a bone to every dog" + print() + print("Analysis of sentence '%s'" % sentence) + print("=" * 50) + trees = cs.parse_with_bindops(sentence, trace=0) + for tree in trees: + semrep = cs.CooperStore(tree.label()["SEM"]) + print() + print("Binding operators:") + print("-" * 15) + for s in semrep.store: + print(s) + print() + print("Core:") + print("-" * 15) + print(semrep.core) + print() + print("S-Retrieval:") + print("-" * 15) + semrep.s_retrieve(trace=True) + print("Readings:") + print("-" * 15) + + for i, reading in enumerate(semrep.readings): + print(f"{i + 1}: {reading}") + + +if __name__ == "__main__": + demo() diff --git a/nltk/sem/drt.py b/nltk/sem/drt.py new file mode 100644 index 0000000..6714923 --- /dev/null +++ b/nltk/sem/drt.py @@ -0,0 +1,1516 @@ +# Natural Language Toolkit: Discourse Representation Theory (DRT) +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +import operator +from functools import reduce +from itertools import chain + +from nltk.sem.logic import ( + APP, + AbstractVariableExpression, + AllExpression, + AndExpression, + ApplicationExpression, + BinaryExpression, + BooleanExpression, + ConstantExpression, + EqualityExpression, + EventVariableExpression, + ExistsExpression, + Expression, + FunctionVariableExpression, + ImpExpression, + IndividualVariableExpression, + LambdaExpression, + LogicParser, + NegatedExpression, + OrExpression, + Tokens, + Variable, + is_eventvar, + is_funcvar, + is_indvar, + unique_variable, +) +from nltk.util import in_idle + + +class DrtTokens(Tokens): + DRS = "DRS" + DRS_CONC = "+" + PRONOUN = "PRO" + OPEN_BRACKET = "[" + CLOSE_BRACKET = "]" + COLON = ":" + + PUNCT = [DRS_CONC, OPEN_BRACKET, CLOSE_BRACKET, COLON] + + SYMBOLS = Tokens.SYMBOLS + PUNCT + + TOKENS = Tokens.TOKENS + [DRS] + PUNCT + + +class DrtParser(LogicParser): + """A lambda calculus expression parser.""" + + def __init__(self): + LogicParser.__init__(self) + + self.operator_precedence = dict( + [(x, 1) for x in DrtTokens.LAMBDA_LIST] + + [(x, 2) for x in DrtTokens.NOT_LIST] + + [(APP, 3)] + + [(x, 4) for x in DrtTokens.EQ_LIST + Tokens.NEQ_LIST] + + [(DrtTokens.COLON, 5)] + + [(DrtTokens.DRS_CONC, 6)] + + [(x, 7) for x in DrtTokens.OR_LIST] + + [(x, 8) for x in DrtTokens.IMP_LIST] + + [(None, 9)] + ) + + def get_all_symbols(self): + """This method exists to be overridden""" + return DrtTokens.SYMBOLS + + def isvariable(self, tok): + return tok not in DrtTokens.TOKENS + + def handle(self, tok, context): + """This method is intended to be overridden for logics that + use different operators or expressions""" + if tok in DrtTokens.NOT_LIST: + return self.handle_negation(tok, context) + + elif tok in DrtTokens.LAMBDA_LIST: + return self.handle_lambda(tok, context) + + elif tok == DrtTokens.OPEN: + if self.inRange(0) and self.token(0) == DrtTokens.OPEN_BRACKET: + return self.handle_DRS(tok, context) + else: + return self.handle_open(tok, context) + + elif tok.upper() == DrtTokens.DRS: + self.assertNextToken(DrtTokens.OPEN) + return self.handle_DRS(tok, context) + + elif self.isvariable(tok): + if self.inRange(0) and self.token(0) == DrtTokens.COLON: + return self.handle_prop(tok, context) + else: + return self.handle_variable(tok, context) + + def make_NegatedExpression(self, expression): + return DrtNegatedExpression(expression) + + def handle_DRS(self, tok, context): + # a DRS + refs = self.handle_refs() + if ( + self.inRange(0) and self.token(0) == DrtTokens.COMMA + ): # if there is a comma (it's optional) + self.token() # swallow the comma + conds = self.handle_conds(context) + self.assertNextToken(DrtTokens.CLOSE) + return DRS(refs, conds, None) + + def handle_refs(self): + self.assertNextToken(DrtTokens.OPEN_BRACKET) + refs = [] + while self.inRange(0) and self.token(0) != DrtTokens.CLOSE_BRACKET: + # Support expressions like: DRS([x y],C) == DRS([x,y],C) + if refs and self.token(0) == DrtTokens.COMMA: + self.token() # swallow the comma + refs.append(self.get_next_token_variable("quantified")) + self.assertNextToken(DrtTokens.CLOSE_BRACKET) + return refs + + def handle_conds(self, context): + self.assertNextToken(DrtTokens.OPEN_BRACKET) + conds = [] + while self.inRange(0) and self.token(0) != DrtTokens.CLOSE_BRACKET: + # Support expressions like: DRS([x y],C) == DRS([x, y],C) + if conds and self.token(0) == DrtTokens.COMMA: + self.token() # swallow the comma + conds.append(self.process_next_expression(context)) + self.assertNextToken(DrtTokens.CLOSE_BRACKET) + return conds + + def handle_prop(self, tok, context): + variable = self.make_VariableExpression(tok) + self.assertNextToken(":") + drs = self.process_next_expression(DrtTokens.COLON) + return DrtProposition(variable, drs) + + def make_EqualityExpression(self, first, second): + """This method serves as a hook for other logic parsers that + have different equality expression classes""" + return DrtEqualityExpression(first, second) + + def get_BooleanExpression_factory(self, tok): + """This method serves as a hook for other logic parsers that + have different boolean operators""" + if tok == DrtTokens.DRS_CONC: + return lambda first, second: DrtConcatenation(first, second, None) + elif tok in DrtTokens.OR_LIST: + return DrtOrExpression + elif tok in DrtTokens.IMP_LIST: + + def make_imp_expression(first, second): + if isinstance(first, DRS): + return DRS(first.refs, first.conds, second) + if isinstance(first, DrtConcatenation): + return DrtConcatenation(first.first, first.second, second) + raise Exception("Antecedent of implication must be a DRS") + + return make_imp_expression + else: + return None + + def make_BooleanExpression(self, factory, first, second): + return factory(first, second) + + def make_ApplicationExpression(self, function, argument): + return DrtApplicationExpression(function, argument) + + def make_VariableExpression(self, name): + return DrtVariableExpression(Variable(name)) + + def make_LambdaExpression(self, variables, term): + return DrtLambdaExpression(variables, term) + + +class DrtExpression: + """ + This is the base abstract DRT Expression from which every DRT + Expression extends. + """ + + _drt_parser = DrtParser() + + @classmethod + def fromstring(cls, s): + return cls._drt_parser.parse(s) + + def applyto(self, other): + return DrtApplicationExpression(self, other) + + def __neg__(self): + return DrtNegatedExpression(self) + + def __and__(self, other): + return NotImplemented + + def __or__(self, other): + assert isinstance(other, DrtExpression) + return DrtOrExpression(self, other) + + def __gt__(self, other): + assert isinstance(other, DrtExpression) + if isinstance(self, DRS): + return DRS(self.refs, self.conds, other) + if isinstance(self, DrtConcatenation): + return DrtConcatenation(self.first, self.second, other) + raise Exception("Antecedent of implication must be a DRS") + + def equiv(self, other, prover=None): + """ + Check for logical equivalence. + Pass the expression (self <-> other) to the theorem prover. + If the prover says it is valid, then the self and other are equal. + + :param other: an ``DrtExpression`` to check equality against + :param prover: a ``nltk.inference.api.Prover`` + """ + assert isinstance(other, DrtExpression) + + f1 = self.simplify().fol() + f2 = other.simplify().fol() + return f1.equiv(f2, prover) + + @property + def type(self): + raise AttributeError( + "'%s' object has no attribute 'type'" % self.__class__.__name__ + ) + + def typecheck(self, signature=None): + raise NotImplementedError() + + def __add__(self, other): + return DrtConcatenation(self, other, None) + + def get_refs(self, recursive=False): + """ + Return the set of discourse referents in this DRS. + :param recursive: bool Also find discourse referents in subterms? + :return: list of ``Variable`` objects + """ + raise NotImplementedError() + + def is_pronoun_function(self): + """Is self of the form "PRO(x)"?""" + return ( + isinstance(self, DrtApplicationExpression) + and isinstance(self.function, DrtAbstractVariableExpression) + and self.function.variable.name == DrtTokens.PRONOUN + and isinstance(self.argument, DrtIndividualVariableExpression) + ) + + def make_EqualityExpression(self, first, second): + return DrtEqualityExpression(first, second) + + def make_VariableExpression(self, variable): + return DrtVariableExpression(variable) + + def resolve_anaphora(self): + return resolve_anaphora(self) + + def eliminate_equality(self): + return self.visit_structured(lambda e: e.eliminate_equality(), self.__class__) + + def pretty_format(self): + """ + Draw the DRS + :return: the pretty print string + """ + return "\n".join(self._pretty()) + + def pretty_print(self): + print(self.pretty_format()) + + def draw(self): + DrsDrawer(self).draw() + + +class DRS(DrtExpression, Expression): + """A Discourse Representation Structure.""" + + def __init__(self, refs, conds, consequent=None): + """ + :param refs: list of ``DrtIndividualVariableExpression`` for the + discourse referents + :param conds: list of ``Expression`` for the conditions + """ + self.refs = refs + self.conds = conds + self.consequent = consequent + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + """Replace all instances of variable v with expression E in self, + where v is free in self.""" + if variable in self.refs: + # if a bound variable is the thing being replaced + if not replace_bound: + return self + else: + i = self.refs.index(variable) + if self.consequent: + consequent = self.consequent.replace( + variable, expression, True, alpha_convert + ) + else: + consequent = None + return DRS( + self.refs[:i] + [expression.variable] + self.refs[i + 1 :], + [ + cond.replace(variable, expression, True, alpha_convert) + for cond in self.conds + ], + consequent, + ) + else: + if alpha_convert: + # any bound variable that appears in the expression must + # be alpha converted to avoid a conflict + for ref in set(self.refs) & expression.free(): + newvar = unique_variable(ref) + newvarex = DrtVariableExpression(newvar) + i = self.refs.index(ref) + if self.consequent: + consequent = self.consequent.replace( + ref, newvarex, True, alpha_convert + ) + else: + consequent = None + self = DRS( + self.refs[:i] + [newvar] + self.refs[i + 1 :], + [ + cond.replace(ref, newvarex, True, alpha_convert) + for cond in self.conds + ], + consequent, + ) + + # replace in the conditions + if self.consequent: + consequent = self.consequent.replace( + variable, expression, replace_bound, alpha_convert + ) + else: + consequent = None + return DRS( + self.refs, + [ + cond.replace(variable, expression, replace_bound, alpha_convert) + for cond in self.conds + ], + consequent, + ) + + def free(self): + """:see: Expression.free()""" + conds_free = reduce(operator.or_, [c.free() for c in self.conds], set()) + if self.consequent: + conds_free.update(self.consequent.free()) + return conds_free - set(self.refs) + + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + if recursive: + conds_refs = self.refs + list( + chain.from_iterable(c.get_refs(True) for c in self.conds) + ) + if self.consequent: + conds_refs.extend(self.consequent.get_refs(True)) + return conds_refs + else: + return self.refs + + def visit(self, function, combinator): + """:see: Expression.visit()""" + parts = list(map(function, self.conds)) + if self.consequent: + parts.append(function(self.consequent)) + return combinator(parts) + + def visit_structured(self, function, combinator): + """:see: Expression.visit_structured()""" + consequent = function(self.consequent) if self.consequent else None + return combinator(self.refs, list(map(function, self.conds)), consequent) + + def eliminate_equality(self): + drs = self + i = 0 + while i < len(drs.conds): + cond = drs.conds[i] + if ( + isinstance(cond, EqualityExpression) + and isinstance(cond.first, AbstractVariableExpression) + and isinstance(cond.second, AbstractVariableExpression) + ): + drs = DRS( + list(set(drs.refs) - {cond.second.variable}), + drs.conds[:i] + drs.conds[i + 1 :], + drs.consequent, + ) + if cond.second.variable != cond.first.variable: + drs = drs.replace(cond.second.variable, cond.first, False, False) + i = 0 + i -= 1 + i += 1 + + conds = [] + for cond in drs.conds: + new_cond = cond.eliminate_equality() + new_cond_simp = new_cond.simplify() + if ( + not isinstance(new_cond_simp, DRS) + or new_cond_simp.refs + or new_cond_simp.conds + or new_cond_simp.consequent + ): + conds.append(new_cond) + + consequent = drs.consequent.eliminate_equality() if drs.consequent else None + return DRS(drs.refs, conds, consequent) + + def fol(self): + if self.consequent: + accum = None + if self.conds: + accum = reduce(AndExpression, [c.fol() for c in self.conds]) + + if accum: + accum = ImpExpression(accum, self.consequent.fol()) + else: + accum = self.consequent.fol() + + for ref in self.refs[::-1]: + accum = AllExpression(ref, accum) + + return accum + + else: + if not self.conds: + raise Exception("Cannot convert DRS with no conditions to FOL.") + accum = reduce(AndExpression, [c.fol() for c in self.conds]) + for ref in map(Variable, self._order_ref_strings(self.refs)[::-1]): + accum = ExistsExpression(ref, accum) + return accum + + def _pretty(self): + refs_line = " ".join(self._order_ref_strings(self.refs)) + + cond_lines = [ + cond + for cond_line in [ + filter(lambda s: s.strip(), cond._pretty()) for cond in self.conds + ] + for cond in cond_line + ] + length = max([len(refs_line)] + list(map(len, cond_lines))) + drs = ( + [ + " _" + "_" * length + "_ ", + "| " + refs_line.ljust(length) + " |", + "|-" + "-" * length + "-|", + ] + + ["| " + line.ljust(length) + " |" for line in cond_lines] + + ["|_" + "_" * length + "_|"] + ) + if self.consequent: + return DrtBinaryExpression._assemble_pretty( + drs, DrtTokens.IMP, self.consequent._pretty() + ) + return drs + + def _order_ref_strings(self, refs): + strings = ["%s" % ref for ref in refs] + ind_vars = [] + func_vars = [] + event_vars = [] + other_vars = [] + for s in strings: + if is_indvar(s): + ind_vars.append(s) + elif is_funcvar(s): + func_vars.append(s) + elif is_eventvar(s): + event_vars.append(s) + else: + other_vars.append(s) + return ( + sorted(other_vars) + + sorted(event_vars, key=lambda v: int([v[2:], -1][len(v[2:]) == 0])) + + sorted(func_vars, key=lambda v: (v[0], int([v[1:], -1][len(v[1:]) == 0]))) + + sorted(ind_vars, key=lambda v: (v[0], int([v[1:], -1][len(v[1:]) == 0]))) + ) + + def __eq__(self, other): + r"""Defines equality modulo alphabetic variance. + If we are comparing \x.M and \y.N, then check equality of M and N[x/y].""" + if isinstance(other, DRS): + if len(self.refs) == len(other.refs): + converted_other = other + for r1, r2 in zip(self.refs, converted_other.refs): + varex = self.make_VariableExpression(r1) + converted_other = converted_other.replace(r2, varex, True) + if self.consequent == converted_other.consequent and len( + self.conds + ) == len(converted_other.conds): + for c1, c2 in zip(self.conds, converted_other.conds): + if not (c1 == c2): + return False + return True + return False + + def __ne__(self, other): + return not self == other + + __hash__ = Expression.__hash__ + + def __str__(self): + drs = "([{}],[{}])".format( + ",".join(self._order_ref_strings(self.refs)), + ", ".join("%s" % cond for cond in self.conds), + ) # map(str, self.conds))) + if self.consequent: + return ( + DrtTokens.OPEN + + drs + + " " + + DrtTokens.IMP + + " " + + "%s" % self.consequent + + DrtTokens.CLOSE + ) + return drs + + +def DrtVariableExpression(variable): + """ + This is a factory method that instantiates and returns a subtype of + ``DrtAbstractVariableExpression`` appropriate for the given variable. + """ + if is_indvar(variable.name): + return DrtIndividualVariableExpression(variable) + elif is_funcvar(variable.name): + return DrtFunctionVariableExpression(variable) + elif is_eventvar(variable.name): + return DrtEventVariableExpression(variable) + else: + return DrtConstantExpression(variable) + + +class DrtAbstractVariableExpression(DrtExpression, AbstractVariableExpression): + def fol(self): + return self + + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + return [] + + def _pretty(self): + s = "%s" % self + blank = " " * len(s) + return [blank, blank, s, blank] + + def eliminate_equality(self): + return self + + +class DrtIndividualVariableExpression( + DrtAbstractVariableExpression, IndividualVariableExpression +): + pass + + +class DrtFunctionVariableExpression( + DrtAbstractVariableExpression, FunctionVariableExpression +): + pass + + +class DrtEventVariableExpression( + DrtIndividualVariableExpression, EventVariableExpression +): + pass + + +class DrtConstantExpression(DrtAbstractVariableExpression, ConstantExpression): + pass + + +class DrtProposition(DrtExpression, Expression): + def __init__(self, variable, drs): + self.variable = variable + self.drs = drs + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + if self.variable == variable: + assert isinstance( + expression, DrtAbstractVariableExpression + ), "Can only replace a proposition label with a variable" + return DrtProposition( + expression.variable, + self.drs.replace(variable, expression, replace_bound, alpha_convert), + ) + else: + return DrtProposition( + self.variable, + self.drs.replace(variable, expression, replace_bound, alpha_convert), + ) + + def eliminate_equality(self): + return DrtProposition(self.variable, self.drs.eliminate_equality()) + + def get_refs(self, recursive=False): + return self.drs.get_refs(True) if recursive else [] + + def __eq__(self, other): + return ( + self.__class__ == other.__class__ + and self.variable == other.variable + and self.drs == other.drs + ) + + def __ne__(self, other): + return not self == other + + __hash__ = Expression.__hash__ + + def fol(self): + return self.drs.fol() + + def _pretty(self): + drs_s = self.drs._pretty() + blank = " " * len("%s" % self.variable) + return ( + [blank + " " + line for line in drs_s[:1]] + + ["%s" % self.variable + ":" + line for line in drs_s[1:2]] + + [blank + " " + line for line in drs_s[2:]] + ) + + def visit(self, function, combinator): + """:see: Expression.visit()""" + return combinator([function(self.drs)]) + + def visit_structured(self, function, combinator): + """:see: Expression.visit_structured()""" + return combinator(self.variable, function(self.drs)) + + def __str__(self): + return f"prop({self.variable}, {self.drs})" + + +class DrtNegatedExpression(DrtExpression, NegatedExpression): + def fol(self): + return NegatedExpression(self.term.fol()) + + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + return self.term.get_refs(recursive) + + def _pretty(self): + term_lines = self.term._pretty() + return ( + [" " + line for line in term_lines[:2]] + + ["__ " + line for line in term_lines[2:3]] + + [" | " + line for line in term_lines[3:4]] + + [" " + line for line in term_lines[4:]] + ) + + +class DrtLambdaExpression(DrtExpression, LambdaExpression): + def alpha_convert(self, newvar): + """Rename all occurrences of the variable introduced by this variable + binder in the expression to ``newvar``. + :param newvar: ``Variable``, for the new variable + """ + return self.__class__( + newvar, + self.term.replace(self.variable, DrtVariableExpression(newvar), True), + ) + + def fol(self): + return LambdaExpression(self.variable, self.term.fol()) + + def _pretty(self): + variables = [self.variable] + term = self.term + while term.__class__ == self.__class__: + variables.append(term.variable) + term = term.term + var_string = " ".join("%s" % v for v in variables) + DrtTokens.DOT + term_lines = term._pretty() + blank = " " * len(var_string) + return ( + [" " + blank + line for line in term_lines[:1]] + + [r" \ " + blank + line for line in term_lines[1:2]] + + [r" /\ " + var_string + line for line in term_lines[2:3]] + + [" " + blank + line for line in term_lines[3:]] + ) + + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + return ( + [self.variable] + self.term.get_refs(True) if recursive else [self.variable] + ) + + +class DrtBinaryExpression(DrtExpression, BinaryExpression): + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + return ( + self.first.get_refs(True) + self.second.get_refs(True) if recursive else [] + ) + + def _pretty(self): + return DrtBinaryExpression._assemble_pretty( + self._pretty_subex(self.first), + self.getOp(), + self._pretty_subex(self.second), + ) + + @staticmethod + def _assemble_pretty(first_lines, op, second_lines): + max_lines = max(len(first_lines), len(second_lines)) + first_lines = _pad_vertically(first_lines, max_lines) + second_lines = _pad_vertically(second_lines, max_lines) + blank = " " * len(op) + first_second_lines = list(zip(first_lines, second_lines)) + return ( + [ + " " + first_line + " " + blank + " " + second_line + " " + for first_line, second_line in first_second_lines[:2] + ] + + [ + "(" + first_line + " " + op + " " + second_line + ")" + for first_line, second_line in first_second_lines[2:3] + ] + + [ + " " + first_line + " " + blank + " " + second_line + " " + for first_line, second_line in first_second_lines[3:] + ] + ) + + def _pretty_subex(self, subex): + return subex._pretty() + + +class DrtBooleanExpression(DrtBinaryExpression, BooleanExpression): + pass + + +class DrtOrExpression(DrtBooleanExpression, OrExpression): + def fol(self): + return OrExpression(self.first.fol(), self.second.fol()) + + def _pretty_subex(self, subex): + if isinstance(subex, DrtOrExpression): + return [line[1:-1] for line in subex._pretty()] + return DrtBooleanExpression._pretty_subex(self, subex) + + +class DrtEqualityExpression(DrtBinaryExpression, EqualityExpression): + def fol(self): + return EqualityExpression(self.first.fol(), self.second.fol()) + + +class DrtConcatenation(DrtBooleanExpression): + """DRS of the form '(DRS + DRS)'""" + + def __init__(self, first, second, consequent=None): + DrtBooleanExpression.__init__(self, first, second) + self.consequent = consequent + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + """Replace all instances of variable v with expression E in self, + where v is free in self.""" + first = self.first + second = self.second + consequent = self.consequent + + # If variable is bound + if variable in self.get_refs(): + if replace_bound: + first = first.replace( + variable, expression, replace_bound, alpha_convert + ) + second = second.replace( + variable, expression, replace_bound, alpha_convert + ) + if consequent: + consequent = consequent.replace( + variable, expression, replace_bound, alpha_convert + ) + else: + if alpha_convert: + # alpha convert every ref that is free in 'expression' + for ref in set(self.get_refs(True)) & expression.free(): + v = DrtVariableExpression(unique_variable(ref)) + first = first.replace(ref, v, True, alpha_convert) + second = second.replace(ref, v, True, alpha_convert) + if consequent: + consequent = consequent.replace(ref, v, True, alpha_convert) + + first = first.replace(variable, expression, replace_bound, alpha_convert) + second = second.replace(variable, expression, replace_bound, alpha_convert) + if consequent: + consequent = consequent.replace( + variable, expression, replace_bound, alpha_convert + ) + + return self.__class__(first, second, consequent) + + def eliminate_equality(self): + # TODO: at some point. for now, simplify. + drs = self.simplify() + assert not isinstance(drs, DrtConcatenation) + return drs.eliminate_equality() + + def simplify(self): + first = self.first.simplify() + second = self.second.simplify() + consequent = self.consequent.simplify() if self.consequent else None + + if isinstance(first, DRS) and isinstance(second, DRS): + # For any ref that is in both 'first' and 'second' + for ref in set(first.get_refs(True)) & set(second.get_refs(True)): + # alpha convert the ref in 'second' to prevent collision + newvar = DrtVariableExpression(unique_variable(ref)) + second = second.replace(ref, newvar, True) + + return DRS(first.refs + second.refs, first.conds + second.conds, consequent) + else: + return self.__class__(first, second, consequent) + + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + refs = self.first.get_refs(recursive) + self.second.get_refs(recursive) + if self.consequent and recursive: + refs.extend(self.consequent.get_refs(True)) + return refs + + def getOp(self): + return DrtTokens.DRS_CONC + + def __eq__(self, other): + r"""Defines equality modulo alphabetic variance. + If we are comparing \x.M and \y.N, then check equality of M and N[x/y].""" + if isinstance(other, DrtConcatenation): + self_refs = self.get_refs() + other_refs = other.get_refs() + if len(self_refs) == len(other_refs): + converted_other = other + for r1, r2 in zip(self_refs, other_refs): + varex = self.make_VariableExpression(r1) + converted_other = converted_other.replace(r2, varex, True) + return ( + self.first == converted_other.first + and self.second == converted_other.second + and self.consequent == converted_other.consequent + ) + return False + + def __ne__(self, other): + return not self == other + + __hash__ = DrtBooleanExpression.__hash__ + + def fol(self): + e = AndExpression(self.first.fol(), self.second.fol()) + if self.consequent: + e = ImpExpression(e, self.consequent.fol()) + return e + + def _pretty(self): + drs = DrtBinaryExpression._assemble_pretty( + self._pretty_subex(self.first), + self.getOp(), + self._pretty_subex(self.second), + ) + if self.consequent: + drs = DrtBinaryExpression._assemble_pretty( + drs, DrtTokens.IMP, self.consequent._pretty() + ) + return drs + + def _pretty_subex(self, subex): + if isinstance(subex, DrtConcatenation): + return [line[1:-1] for line in subex._pretty()] + return DrtBooleanExpression._pretty_subex(self, subex) + + def visit(self, function, combinator): + """:see: Expression.visit()""" + if self.consequent: + return combinator( + [function(self.first), function(self.second), function(self.consequent)] + ) + else: + return combinator([function(self.first), function(self.second)]) + + def __str__(self): + first = self._str_subex(self.first) + second = self._str_subex(self.second) + drs = Tokens.OPEN + first + " " + self.getOp() + " " + second + Tokens.CLOSE + if self.consequent: + return ( + DrtTokens.OPEN + + drs + + " " + + DrtTokens.IMP + + " " + + "%s" % self.consequent + + DrtTokens.CLOSE + ) + return drs + + def _str_subex(self, subex): + s = "%s" % subex + if isinstance(subex, DrtConcatenation) and subex.consequent is None: + return s[1:-1] + return s + + +class DrtApplicationExpression(DrtExpression, ApplicationExpression): + def fol(self): + return ApplicationExpression(self.function.fol(), self.argument.fol()) + + def get_refs(self, recursive=False): + """:see: AbstractExpression.get_refs()""" + return ( + self.function.get_refs(True) + self.argument.get_refs(True) + if recursive + else [] + ) + + def _pretty(self): + function, args = self.uncurry() + function_lines = function._pretty() + args_lines = [arg._pretty() for arg in args] + max_lines = max(map(len, [function_lines] + args_lines)) + function_lines = _pad_vertically(function_lines, max_lines) + args_lines = [_pad_vertically(arg_lines, max_lines) for arg_lines in args_lines] + func_args_lines = list(zip(function_lines, list(zip(*args_lines)))) + return ( + [ + func_line + " " + " ".join(args_line) + " " + for func_line, args_line in func_args_lines[:2] + ] + + [ + func_line + "(" + ",".join(args_line) + ")" + for func_line, args_line in func_args_lines[2:3] + ] + + [ + func_line + " " + " ".join(args_line) + " " + for func_line, args_line in func_args_lines[3:] + ] + ) + + +def _pad_vertically(lines, max_lines): + pad_line = [" " * len(lines[0])] + return lines + pad_line * (max_lines - len(lines)) + + +class PossibleAntecedents(list, DrtExpression, Expression): + def free(self): + """Set of free variables.""" + return set(self) + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + """Replace all instances of variable v with expression E in self, + where v is free in self.""" + result = PossibleAntecedents() + for item in self: + if item == variable: + self.append(expression) + else: + self.append(item) + return result + + def _pretty(self): + s = "%s" % self + blank = " " * len(s) + return [blank, blank, s] + + def __str__(self): + return "[" + ",".join("%s" % it for it in self) + "]" + + +class AnaphoraResolutionException(Exception): + pass + + +#: Upper bound on the number of (pronoun, discourse-referent) examinations a +#: single ``resolve_anaphora`` call may perform. Each pronoun condition scans +#: every referent on the trail and retains the compatible ones as candidate +#: antecedents, so a DRS with N referents and N pronouns costs O(N**2) time and +#: retained memory; a small crafted DRS string then pins the CPU and exhausts +#: memory (CWE-770; CVE-2026-12873). Once this many examinations have been made, +#: resolution raises ``AnaphoraResolutionException`` instead of running +#: unbounded. Raise it if you legitimately need to resolve a larger discourse. +MAX_ANAPHORA_OPERATIONS = 1_000_000 + + +class _AnaphoraBudget: + """Counts candidate-antecedent examinations and aborts runaway resolution. + + The whole resolution shares one budget, so it bounds the total O(N**2) work + regardless of how the DRS is structured. + """ + + __slots__ = ("remaining", "limit") + + def __init__(self, limit): + # ``limit`` comes from the module-global ``MAX_ANAPHORA_OPERATIONS``, + # which callers may override; validate it so a bad value fails clearly + # here rather than as an obscure error mid-resolution. + if not isinstance(limit, int) or limit < 1: + raise ValueError( + f"MAX_ANAPHORA_OPERATIONS must be a positive int, got {limit!r}" + ) + self.remaining = limit + self.limit = limit + + def spend(self): + self.remaining -= 1 + if self.remaining < 0: + raise AnaphoraResolutionException( + "Refusing to resolve anaphora: examining candidate antecedents " + "exceeded the limit of %d (pronoun, referent) steps, which a DRS " + "with many referents and pronouns reaches at O(n**2) time and " + "memory (CWE-770). Resolve a smaller discourse, or raise " + "nltk.sem.drt.MAX_ANAPHORA_OPERATIONS." % self.limit + ) + + +def resolve_anaphora(expression, trail=[], budget=None): + # A shared budget bounds the total (pronoun, referent) examinations across + # the whole resolution: each pronoun scans every referent on the trail and + # retains the compatible ones, so a DRS with many referents and pronouns is + # O(N**2) in time and retained memory and a small crafted string can exhaust + # the process (CWE-770; CVE-2026-12873). + if budget is None: + budget = _AnaphoraBudget(MAX_ANAPHORA_OPERATIONS) + if isinstance(expression, ApplicationExpression): + if expression.is_pronoun_function(): + possible_antecedents = PossibleAntecedents() + for ancestor in trail: + for ref in ancestor.get_refs(): + budget.spend() + refex = expression.make_VariableExpression(ref) + + # ========================================================== + # Don't allow resolution to itself or other types + # ========================================================== + if refex.__class__ == expression.argument.__class__ and not ( + refex == expression.argument + ): + possible_antecedents.append(refex) + + if len(possible_antecedents) == 1: + resolution = possible_antecedents[0] + else: + resolution = possible_antecedents + return expression.make_EqualityExpression(expression.argument, resolution) + else: + r_function = resolve_anaphora( + expression.function, trail + [expression], budget + ) + r_argument = resolve_anaphora( + expression.argument, trail + [expression], budget + ) + return expression.__class__(r_function, r_argument) + + elif isinstance(expression, DRS): + r_conds = [] + for cond in expression.conds: + r_cond = resolve_anaphora(cond, trail + [expression], budget) + + # if the condition is of the form '(x = [])' then raise exception + if isinstance(r_cond, EqualityExpression): + if isinstance(r_cond.first, PossibleAntecedents): + # Reverse the order so that the variable is on the left + temp = r_cond.first + r_cond.first = r_cond.second + r_cond.second = temp + if isinstance(r_cond.second, PossibleAntecedents): + if not r_cond.second: + raise AnaphoraResolutionException( + "Variable '%s' does not " + "resolve to anything." % r_cond.first + ) + + r_conds.append(r_cond) + if expression.consequent: + consequent = resolve_anaphora( + expression.consequent, trail + [expression], budget + ) + else: + consequent = None + return expression.__class__(expression.refs, r_conds, consequent) + + elif isinstance(expression, AbstractVariableExpression): + return expression + + elif isinstance(expression, NegatedExpression): + return expression.__class__( + resolve_anaphora(expression.term, trail + [expression], budget) + ) + + elif isinstance(expression, DrtConcatenation): + if expression.consequent: + consequent = resolve_anaphora( + expression.consequent, trail + [expression], budget + ) + else: + consequent = None + return expression.__class__( + resolve_anaphora(expression.first, trail + [expression], budget), + resolve_anaphora(expression.second, trail + [expression], budget), + consequent, + ) + + elif isinstance(expression, BinaryExpression): + return expression.__class__( + resolve_anaphora(expression.first, trail + [expression], budget), + resolve_anaphora(expression.second, trail + [expression], budget), + ) + + elif isinstance(expression, LambdaExpression): + return expression.__class__( + expression.variable, + resolve_anaphora(expression.term, trail + [expression], budget), + ) + + +class DrsDrawer: + BUFFER = 3 # Space between elements + TOPSPACE = 10 # Space above whole DRS + OUTERSPACE = 6 # Space to the left, right, and bottom of the while DRS + + def __init__(self, drs, size_canvas=True, canvas=None): + """ + :param drs: ``DrtExpression``, The DRS to be drawn + :param size_canvas: bool, True if the canvas size should be the exact size of the DRS + :param canvas: ``Canvas`` The canvas on which to draw the DRS. If none is given, create a new canvas. + """ + master = None + if not canvas: + + # Only import tkinter if the user has indicated that they + # want to draw a UI. See issue #2949 for more info. + from tkinter import Canvas, Tk + from tkinter.font import Font + + master = Tk() + master.title("DRT") + + font = Font(family="helvetica", size=12) + + if size_canvas: + canvas = Canvas(master, width=0, height=0) + canvas.font = font + self.canvas = canvas + (right, bottom) = self._visit(drs, self.OUTERSPACE, self.TOPSPACE) + + width = max(right + self.OUTERSPACE, 100) + height = bottom + self.OUTERSPACE + canvas = Canvas(master, width=width, height=height) # , bg='white') + else: + canvas = Canvas(master, width=300, height=300) + + canvas.pack() + canvas.font = font + + self.canvas = canvas + self.drs = drs + self.master = master + + def _get_text_height(self): + """Get the height of a line of text""" + return self.canvas.font.metrics("linespace") + + def draw(self, x=OUTERSPACE, y=TOPSPACE): + """Draw the DRS""" + self._handle(self.drs, self._draw_command, x, y) + + if self.master and not in_idle(): + self.master.mainloop() + else: + return self._visit(self.drs, x, y) + + def _visit(self, expression, x, y): + """ + Return the bottom-rightmost point without actually drawing the item + + :param expression: the item to visit + :param x: the top of the current drawing area + :param y: the left side of the current drawing area + :return: the bottom-rightmost point + """ + return self._handle(expression, self._visit_command, x, y) + + def _draw_command(self, item, x, y): + """ + Draw the given item at the given location + + :param item: the item to draw + :param x: the top of the current drawing area + :param y: the left side of the current drawing area + :return: the bottom-rightmost point + """ + if isinstance(item, str): + self.canvas.create_text(x, y, anchor="nw", font=self.canvas.font, text=item) + elif isinstance(item, tuple): + # item is the lower-right of a box + (right, bottom) = item + self.canvas.create_rectangle(x, y, right, bottom) + horiz_line_y = ( + y + self._get_text_height() + (self.BUFFER * 2) + ) # the line separating refs from conds + self.canvas.create_line(x, horiz_line_y, right, horiz_line_y) + + return self._visit_command(item, x, y) + + def _visit_command(self, item, x, y): + """ + Return the bottom-rightmost point without actually drawing the item + + :param item: the item to visit + :param x: the top of the current drawing area + :param y: the left side of the current drawing area + :return: the bottom-rightmost point + """ + if isinstance(item, str): + return (x + self.canvas.font.measure(item), y + self._get_text_height()) + elif isinstance(item, tuple): + return item + + def _handle(self, expression, command, x=0, y=0): + """ + :param expression: the expression to handle + :param command: the function to apply, either _draw_command or _visit_command + :param x: the top of the current drawing area + :param y: the left side of the current drawing area + :return: the bottom-rightmost point + """ + if command == self._visit_command: + # if we don't need to draw the item, then we can use the cached values + try: + # attempt to retrieve cached values + right = expression._drawing_width + x + bottom = expression._drawing_height + y + return (right, bottom) + except AttributeError: + # the values have not been cached yet, so compute them + pass + + if isinstance(expression, DrtAbstractVariableExpression): + factory = self._handle_VariableExpression + elif isinstance(expression, DRS): + factory = self._handle_DRS + elif isinstance(expression, DrtNegatedExpression): + factory = self._handle_NegatedExpression + elif isinstance(expression, DrtLambdaExpression): + factory = self._handle_LambdaExpression + elif isinstance(expression, BinaryExpression): + factory = self._handle_BinaryExpression + elif isinstance(expression, DrtApplicationExpression): + factory = self._handle_ApplicationExpression + elif isinstance(expression, PossibleAntecedents): + factory = self._handle_VariableExpression + elif isinstance(expression, DrtProposition): + factory = self._handle_DrtProposition + else: + raise Exception(expression.__class__.__name__) + + (right, bottom) = factory(expression, command, x, y) + + # cache the values + expression._drawing_width = right - x + expression._drawing_height = bottom - y + + return (right, bottom) + + def _handle_VariableExpression(self, expression, command, x, y): + return command("%s" % expression, x, y) + + def _handle_NegatedExpression(self, expression, command, x, y): + # Find the width of the negation symbol + right = self._visit_command(DrtTokens.NOT, x, y)[0] + + # Handle term + (right, bottom) = self._handle(expression.term, command, right, y) + + # Handle variables now that we know the y-coordinate + command( + DrtTokens.NOT, + x, + self._get_centered_top(y, bottom - y, self._get_text_height()), + ) + + return (right, bottom) + + def _handle_DRS(self, expression, command, x, y): + left = x + self.BUFFER # indent the left side + bottom = y + self.BUFFER # indent the top + + # Handle Discourse Referents + if expression.refs: + refs = " ".join("%s" % r for r in expression.refs) + else: + refs = " " + (max_right, bottom) = command(refs, left, bottom) + bottom += self.BUFFER * 2 + + # Handle Conditions + if expression.conds: + for cond in expression.conds: + (right, bottom) = self._handle(cond, command, left, bottom) + max_right = max(max_right, right) + bottom += self.BUFFER + else: + bottom += self._get_text_height() + self.BUFFER + + # Handle Box + max_right += self.BUFFER + return command((max_right, bottom), x, y) + + def _handle_ApplicationExpression(self, expression, command, x, y): + function, args = expression.uncurry() + if not isinstance(function, DrtAbstractVariableExpression): + # It's not a predicate expression ("P(x,y)"), so leave arguments curried + function = expression.function + args = [expression.argument] + + # Get the max bottom of any element on the line + function_bottom = self._visit(function, x, y)[1] + max_bottom = max( + [function_bottom] + [self._visit(arg, x, y)[1] for arg in args] + ) + + line_height = max_bottom - y + + # Handle 'function' + function_drawing_top = self._get_centered_top( + y, line_height, function._drawing_height + ) + right = self._handle(function, command, x, function_drawing_top)[0] + + # Handle open paren + centred_string_top = self._get_centered_top( + y, line_height, self._get_text_height() + ) + right = command(DrtTokens.OPEN, right, centred_string_top)[0] + + # Handle each arg + for i, arg in enumerate(args): + arg_drawing_top = self._get_centered_top( + y, line_height, arg._drawing_height + ) + right = self._handle(arg, command, right, arg_drawing_top)[0] + + if i + 1 < len(args): + # since it's not the last arg, add a comma + right = command(DrtTokens.COMMA + " ", right, centred_string_top)[0] + + # Handle close paren + right = command(DrtTokens.CLOSE, right, centred_string_top)[0] + + return (right, max_bottom) + + def _handle_LambdaExpression(self, expression, command, x, y): + # Find the width of the lambda symbol and abstracted variables + variables = DrtTokens.LAMBDA + "%s" % expression.variable + DrtTokens.DOT + right = self._visit_command(variables, x, y)[0] + + # Handle term + (right, bottom) = self._handle(expression.term, command, right, y) + + # Handle variables now that we know the y-coordinate + command( + variables, x, self._get_centered_top(y, bottom - y, self._get_text_height()) + ) + + return (right, bottom) + + def _handle_BinaryExpression(self, expression, command, x, y): + # Get the full height of the line, based on the operands + first_height = self._visit(expression.first, 0, 0)[1] + second_height = self._visit(expression.second, 0, 0)[1] + line_height = max(first_height, second_height) + + # Handle open paren + centred_string_top = self._get_centered_top( + y, line_height, self._get_text_height() + ) + right = command(DrtTokens.OPEN, x, centred_string_top)[0] + + # Handle the first operand + first_height = expression.first._drawing_height + (right, first_bottom) = self._handle( + expression.first, + command, + right, + self._get_centered_top(y, line_height, first_height), + ) + + # Handle the operator + right = command(" %s " % expression.getOp(), right, centred_string_top)[0] + + # Handle the second operand + second_height = expression.second._drawing_height + (right, second_bottom) = self._handle( + expression.second, + command, + right, + self._get_centered_top(y, line_height, second_height), + ) + + # Handle close paren + right = command(DrtTokens.CLOSE, right, centred_string_top)[0] + + return (right, max(first_bottom, second_bottom)) + + def _handle_DrtProposition(self, expression, command, x, y): + # Find the width of the negation symbol + right = command(expression.variable, x, y)[0] + + # Handle term + (right, bottom) = self._handle(expression.term, command, right, y) + + return (right, bottom) + + def _get_centered_top(self, top, full_height, item_height): + """Get the y-coordinate of the point that a figure should start at if + its height is 'item_height' and it needs to be centered in an area that + starts at 'top' and is 'full_height' tall.""" + return top + (full_height - item_height) / 2 + + +def demo(): + print("=" * 20 + "TEST PARSE" + "=" * 20) + dexpr = DrtExpression.fromstring + print(dexpr(r"([x,y],[sees(x,y)])")) + print(dexpr(r"([x],[man(x), walks(x)])")) + print(dexpr(r"\x.\y.([],[sees(x,y)])")) + print(dexpr(r"\x.([],[walks(x)])(john)")) + print(dexpr(r"(([x],[walks(x)]) + ([y],[runs(y)]))")) + print(dexpr(r"(([],[walks(x)]) -> ([],[runs(x)]))")) + print(dexpr(r"([x],[PRO(x), sees(John,x)])")) + print(dexpr(r"([x],[man(x), -([],[walks(x)])])")) + print(dexpr(r"([],[(([x],[man(x)]) -> ([],[walks(x)]))])")) + + print("=" * 20 + "Test fol()" + "=" * 20) + print(dexpr(r"([x,y],[sees(x,y)])").fol()) + + print("=" * 20 + "Test alpha conversion and lambda expression equality" + "=" * 20) + e1 = dexpr(r"\x.([],[P(x)])") + print(e1) + e2 = e1.alpha_convert(Variable("z")) + print(e2) + print(e1 == e2) + + print("=" * 20 + "Test resolve_anaphora()" + "=" * 20) + print(resolve_anaphora(dexpr(r"([x,y,z],[dog(x), cat(y), walks(z), PRO(z)])"))) + print( + resolve_anaphora(dexpr(r"([],[(([x],[dog(x)]) -> ([y],[walks(y), PRO(y)]))])")) + ) + print(resolve_anaphora(dexpr(r"(([x,y],[]) + ([],[PRO(x)]))"))) + + print("=" * 20 + "Test pretty_print()" + "=" * 20) + dexpr(r"([],[])").pretty_print() + dexpr( + r"([],[([x],[big(x), dog(x)]) -> ([],[bark(x)]) -([x],[walk(x)])])" + ).pretty_print() + dexpr(r"([x,y],[x=y]) + ([z],[dog(z), walk(z)])").pretty_print() + dexpr(r"([],[([x],[]) | ([y],[]) | ([z],[dog(z), walk(z)])])").pretty_print() + dexpr(r"\P.\Q.(([x],[]) + P(x) + Q(x))(\x.([],[dog(x)]))").pretty_print() + + +def test_draw(): + try: + from tkinter import Tk + except ImportError as e: + raise ValueError("tkinter is required, but it's not available.") + + expressions = [ + r"x", + r"([],[])", + r"([x],[])", + r"([x],[man(x)])", + r"([x,y],[sees(x,y)])", + r"([x],[man(x), walks(x)])", + r"\x.([],[man(x), walks(x)])", + r"\x y.([],[sees(x,y)])", + r"([],[(([],[walks(x)]) + ([],[runs(x)]))])", + r"([x],[man(x), -([],[walks(x)])])", + r"([],[(([x],[man(x)]) -> ([],[walks(x)]))])", + ] + + for e in expressions: + d = DrtExpression.fromstring(e) + d.draw() + + +if __name__ == "__main__": + demo() diff --git a/nltk/sem/drt_glue_demo.py b/nltk/sem/drt_glue_demo.py new file mode 100644 index 0000000..b50d49e --- /dev/null +++ b/nltk/sem/drt_glue_demo.py @@ -0,0 +1,553 @@ +# Natural Language Toolkit: GUI Demo for Glue Semantics with Discourse +# Representation Theory (DRT) as meaning language +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +try: + from tkinter import Button, Frame, IntVar, Label, Listbox, Menu, Scrollbar, Tk + from tkinter.font import Font + + from nltk.draw.util import CanvasFrame, ShowText + +except ImportError: + """Ignore ImportError because tkinter might not be available.""" + +from nltk.parse import MaltParser +from nltk.sem.drt import DrsDrawer, DrtVariableExpression +from nltk.sem.glue import DrtGlue +from nltk.sem.logic import Variable +from nltk.tag import RegexpTagger +from nltk.util import in_idle + + +class DrtGlueDemo: + def __init__(self, examples): + # Set up the main window. + self._top = Tk() + self._top.title("DRT Glue Demo") + + # Set up key bindings. + self._init_bindings() + + # Initialize the fonts.self._error = None + self._init_fonts(self._top) + + self._examples = examples + self._readingCache = [None for example in examples] + + # The user can hide the grammar. + self._show_grammar = IntVar(self._top) + self._show_grammar.set(1) + + # Set the data to None + self._curExample = -1 + self._readings = [] + self._drs = None + self._drsWidget = None + self._error = None + + self._init_glue() + + # Create the basic frames. + self._init_menubar(self._top) + self._init_buttons(self._top) + self._init_exampleListbox(self._top) + self._init_readingListbox(self._top) + self._init_canvas(self._top) + + # Resize callback + self._canvas.bind("", self._configure) + + ######################################### + ## Initialization Helpers + ######################################### + + def _init_glue(self): + tagger = RegexpTagger( + [ + ("^(David|Mary|John)$", "NNP"), + ( + "^(walks|sees|eats|chases|believes|gives|sleeps|chases|persuades|tries|seems|leaves)$", + "VB", + ), + ("^(go|order|vanish|find|approach)$", "VB"), + ("^(a)$", "ex_quant"), + ("^(every)$", "univ_quant"), + ("^(sandwich|man|dog|pizza|unicorn|cat|senator)$", "NN"), + ("^(big|gray|former)$", "JJ"), + ("^(him|himself)$", "PRP"), + ] + ) + + depparser = MaltParser(tagger=tagger) + self._glue = DrtGlue(depparser=depparser, remove_duplicates=False) + + def _init_fonts(self, root): + # See: + self._sysfont = Font(font=Button()["font"]) + root.option_add("*Font", self._sysfont) + + # TWhat's our font size (default=same as sysfont) + self._size = IntVar(root) + self._size.set(self._sysfont.cget("size")) + + self._boldfont = Font(family="helvetica", weight="bold", size=self._size.get()) + self._font = Font(family="helvetica", size=self._size.get()) + if self._size.get() < 0: + big = self._size.get() - 2 + else: + big = self._size.get() + 2 + self._bigfont = Font(family="helvetica", weight="bold", size=big) + + def _init_exampleListbox(self, parent): + self._exampleFrame = listframe = Frame(parent) + self._exampleFrame.pack(fill="both", side="left", padx=2) + self._exampleList_label = Label( + self._exampleFrame, font=self._boldfont, text="Examples" + ) + self._exampleList_label.pack() + self._exampleList = Listbox( + self._exampleFrame, + selectmode="single", + relief="groove", + background="white", + foreground="#909090", + font=self._font, + selectforeground="#004040", + selectbackground="#c0f0c0", + ) + + self._exampleList.pack(side="right", fill="both", expand=1) + + for example in self._examples: + self._exampleList.insert("end", (" %s" % example)) + self._exampleList.config(height=min(len(self._examples), 25), width=40) + + # Add a scrollbar if there are more than 25 examples. + if len(self._examples) > 25: + listscroll = Scrollbar(self._exampleFrame, orient="vertical") + self._exampleList.config(yscrollcommand=listscroll.set) + listscroll.config(command=self._exampleList.yview) + listscroll.pack(side="left", fill="y") + + # If they select a example, apply it. + self._exampleList.bind("<>", self._exampleList_select) + + def _init_readingListbox(self, parent): + self._readingFrame = listframe = Frame(parent) + self._readingFrame.pack(fill="both", side="left", padx=2) + self._readingList_label = Label( + self._readingFrame, font=self._boldfont, text="Readings" + ) + self._readingList_label.pack() + self._readingList = Listbox( + self._readingFrame, + selectmode="single", + relief="groove", + background="white", + foreground="#909090", + font=self._font, + selectforeground="#004040", + selectbackground="#c0f0c0", + ) + + self._readingList.pack(side="right", fill="both", expand=1) + + # Add a scrollbar if there are more than 25 examples. + listscroll = Scrollbar(self._readingFrame, orient="vertical") + self._readingList.config(yscrollcommand=listscroll.set) + listscroll.config(command=self._readingList.yview) + listscroll.pack(side="right", fill="y") + + self._populate_readingListbox() + + def _populate_readingListbox(self): + # Populate the listbox with integers + self._readingList.delete(0, "end") + for i in range(len(self._readings)): + self._readingList.insert("end", (" %s" % (i + 1))) + self._readingList.config(height=min(len(self._readings), 25), width=5) + + # If they select a example, apply it. + self._readingList.bind("<>", self._readingList_select) + + def _init_bindings(self): + # Key bindings are a good thing. + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + self._top.bind("", self.destroy) + self._top.bind("n", self.next) + self._top.bind("", self.next) + self._top.bind("p", self.prev) + self._top.bind("", self.prev) + + def _init_buttons(self, parent): + # Set up the frames. + self._buttonframe = buttonframe = Frame(parent) + buttonframe.pack(fill="none", side="bottom", padx=3, pady=2) + Button( + buttonframe, + text="Prev", + background="#90c0d0", + foreground="black", + command=self.prev, + ).pack(side="left") + Button( + buttonframe, + text="Next", + background="#90c0d0", + foreground="black", + command=self.next, + ).pack(side="left") + + def _configure(self, event): + self._autostep = 0 + (x1, y1, x2, y2) = self._cframe.scrollregion() + y2 = event.height - 6 + self._canvas["scrollregion"] = "%d %d %d %d" % (x1, y1, x2, y2) + self._redraw() + + def _init_canvas(self, parent): + self._cframe = CanvasFrame( + parent, + background="white", + # width=525, height=250, + closeenough=10, + border=2, + relief="sunken", + ) + self._cframe.pack(expand=1, fill="both", side="top", pady=2) + canvas = self._canvas = self._cframe.canvas() + + # Initially, there's no tree or text + self._tree = None + self._textwidgets = [] + self._textline = None + + def _init_menubar(self, parent): + menubar = Menu(parent) + + filemenu = Menu(menubar, tearoff=0) + filemenu.add_command( + label="Exit", underline=1, command=self.destroy, accelerator="q" + ) + menubar.add_cascade(label="File", underline=0, menu=filemenu) + + actionmenu = Menu(menubar, tearoff=0) + actionmenu.add_command( + label="Next", underline=0, command=self.next, accelerator="n, Space" + ) + actionmenu.add_command( + label="Previous", underline=0, command=self.prev, accelerator="p, Backspace" + ) + menubar.add_cascade(label="Action", underline=0, menu=actionmenu) + + optionmenu = Menu(menubar, tearoff=0) + optionmenu.add_checkbutton( + label="Remove Duplicates", + underline=0, + variable=self._glue.remove_duplicates, + command=self._toggle_remove_duplicates, + accelerator="r", + ) + menubar.add_cascade(label="Options", underline=0, menu=optionmenu) + + viewmenu = Menu(menubar, tearoff=0) + viewmenu.add_radiobutton( + label="Tiny", + variable=self._size, + underline=0, + value=10, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Small", + variable=self._size, + underline=0, + value=12, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Medium", + variable=self._size, + underline=0, + value=14, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Large", + variable=self._size, + underline=0, + value=18, + command=self.resize, + ) + viewmenu.add_radiobutton( + label="Huge", + variable=self._size, + underline=0, + value=24, + command=self.resize, + ) + menubar.add_cascade(label="View", underline=0, menu=viewmenu) + + helpmenu = Menu(menubar, tearoff=0) + helpmenu.add_command(label="About", underline=0, command=self.about) + menubar.add_cascade(label="Help", underline=0, menu=helpmenu) + + parent.config(menu=menubar) + + ######################################### + ## Main draw procedure + ######################################### + + def _redraw(self): + canvas = self._canvas + + # Delete the old DRS, widgets, etc. + if self._drsWidget is not None: + self._drsWidget.clear() + + if self._drs: + self._drsWidget = DrsWidget(self._canvas, self._drs) + self._drsWidget.draw() + + if self._error: + self._drsWidget = DrsWidget(self._canvas, self._error) + self._drsWidget.draw() + + ######################################### + ## Button Callbacks + ######################################### + + def destroy(self, *e): + self._autostep = 0 + if self._top is None: + return + self._top.destroy() + self._top = None + + def prev(self, *e): + selection = self._readingList.curselection() + readingListSize = self._readingList.size() + + # there are readings + if readingListSize > 0: + # if one reading is currently selected + if len(selection) == 1: + index = int(selection[0]) + + # if it's on (or before) the first item + if index <= 0: + self._select_previous_example() + else: + self._readingList_store_selection(index - 1) + + else: + # select its first reading + self._readingList_store_selection(readingListSize - 1) + + else: + self._select_previous_example() + + def _select_previous_example(self): + # if the current example is not the first example + if self._curExample > 0: + self._exampleList_store_selection(self._curExample - 1) + else: + # go to the last example + self._exampleList_store_selection(len(self._examples) - 1) + + def next(self, *e): + selection = self._readingList.curselection() + readingListSize = self._readingList.size() + + # if there are readings + if readingListSize > 0: + # if one reading is currently selected + if len(selection) == 1: + index = int(selection[0]) + + # if it's on (or past) the last item + if index >= (readingListSize - 1): + self._select_next_example() + else: + self._readingList_store_selection(index + 1) + + else: + # select its first reading + self._readingList_store_selection(0) + + else: + self._select_next_example() + + def _select_next_example(self): + # if the current example is not the last example + if self._curExample < len(self._examples) - 1: + self._exampleList_store_selection(self._curExample + 1) + else: + # go to the first example + self._exampleList_store_selection(0) + + def about(self, *e): + ABOUT = ( + "NLTK Discourse Representation Theory (DRT) Glue Semantics Demo\n" + + "Written by Daniel H. Garrette" + ) + TITLE = "About: NLTK DRT Glue Demo" + try: + from tkinter.messagebox import Message + + Message(message=ABOUT, title=TITLE).show() + except Exception: + ShowText(self._top, TITLE, ABOUT) + + def postscript(self, *e): + self._autostep = 0 + self._cframe.print_to_file() + + def mainloop(self, *args, **kwargs): + """ + Enter the Tkinter mainloop. This function must be called if + this demo is created from a non-interactive program (e.g. + from a secript); otherwise, the demo will close as soon as + the script completes. + """ + if in_idle(): + return + self._top.mainloop(*args, **kwargs) + + def resize(self, size=None): + if size is not None: + self._size.set(size) + size = self._size.get() + self._font.configure(size=-(abs(size))) + self._boldfont.configure(size=-(abs(size))) + self._sysfont.configure(size=-(abs(size))) + self._bigfont.configure(size=-(abs(size + 2))) + self._redraw() + + def _toggle_remove_duplicates(self): + self._glue.remove_duplicates = not self._glue.remove_duplicates + + self._exampleList.selection_clear(0, "end") + self._readings = [] + self._populate_readingListbox() + self._readingCache = [None for ex in self._examples] + self._curExample = -1 + self._error = None + + self._drs = None + self._redraw() + + def _exampleList_select(self, event): + selection = self._exampleList.curselection() + if len(selection) != 1: + return + self._exampleList_store_selection(int(selection[0])) + + def _exampleList_store_selection(self, index): + self._curExample = index + example = self._examples[index] + + self._exampleList.selection_clear(0, "end") + if example: + cache = self._readingCache[index] + if cache: + if isinstance(cache, list): + self._readings = cache + self._error = None + else: + self._readings = [] + self._error = cache + else: + try: + self._readings = self._glue.parse_to_meaning(example) + self._error = None + self._readingCache[index] = self._readings + except Exception as e: + self._readings = [] + self._error = DrtVariableExpression(Variable("Error: " + str(e))) + self._readingCache[index] = self._error + + # add a star to the end of the example + self._exampleList.delete(index) + self._exampleList.insert(index, (" %s *" % example)) + self._exampleList.config( + height=min(len(self._examples), 25), width=40 + ) + + self._populate_readingListbox() + + self._exampleList.selection_set(index) + + self._drs = None + self._redraw() + + def _readingList_select(self, event): + selection = self._readingList.curselection() + if len(selection) != 1: + return + self._readingList_store_selection(int(selection[0])) + + def _readingList_store_selection(self, index): + reading = self._readings[index] + + self._readingList.selection_clear(0, "end") + if reading: + self._readingList.selection_set(index) + + self._drs = reading.simplify().normalize().resolve_anaphora() + + self._redraw() + + +class DrsWidget: + def __init__(self, canvas, drs, **attribs): + self._drs = drs + self._canvas = canvas + canvas.font = Font( + font=canvas.itemcget(canvas.create_text(0, 0, text=""), "font") + ) + canvas._BUFFER = 3 + self.bbox = (0, 0, 0, 0) + + def draw(self): + (right, bottom) = DrsDrawer(self._drs, canvas=self._canvas).draw() + self.bbox = (0, 0, right + 1, bottom + 1) + + def clear(self): + self._canvas.create_rectangle(self.bbox, fill="white", width="0") + + +def demo(): + examples = [ + "John walks", + "David sees Mary", + "David eats a sandwich", + "every man chases a dog", + # 'every man believes a dog yawns', + # 'John gives David a sandwich', + "John chases himself", + # 'John persuades David to order a pizza', + # 'John tries to go', + # 'John tries to find a unicorn', + # 'John seems to vanish', + # 'a unicorn seems to approach', + # 'every big cat leaves', + # 'every gray cat leaves', + # 'every big gray cat leaves', + # 'a former senator leaves', + # 'John likes a cat', + # 'John likes every cat', + # 'he walks', + # 'John walks and he leaves' + ] + DrtGlueDemo(examples).mainloop() + + +if __name__ == "__main__": + demo() diff --git a/nltk/sem/evaluate.py b/nltk/sem/evaluate.py new file mode 100644 index 0000000..5f9f1bd --- /dev/null +++ b/nltk/sem/evaluate.py @@ -0,0 +1,925 @@ +# Natural Language Toolkit: Models for first-order languages with lambda +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein , +# URL: +# For license information, see LICENSE.TXT + +# TODO: +# - fix tracing +# - fix iterator-based approach to existentials + +""" +This module provides data structures for representing first-order +models. +""" + +import inspect +import re +import sys +import textwrap +from pprint import pformat + +from nltk.decorators import decorator # this used in code that is commented out +from nltk.sem.logic import ( + AbstractVariableExpression, + AllExpression, + AndExpression, + ApplicationExpression, + EqualityExpression, + ExistsExpression, + Expression, + IffExpression, + ImpExpression, + IndividualVariableExpression, + IotaExpression, + LambdaExpression, + NegatedExpression, + OrExpression, + Variable, + is_indvar, +) + + +class Error(Exception): + pass + + +class Undefined(Error): + pass + + +def trace(f, *args, **kw): + argspec = inspect.getfullargspec(f) + d = dict(zip(argspec[0], args)) + if d.pop("trace", None): + print() + for item in d.items(): + print("%s => %s" % item) + return f(*args, **kw) + + +def is_rel(s): + """ + Check whether a set represents a relation (of any arity). + + :param s: a set containing tuples of str elements + :type s: set + :rtype: bool + """ + # we have the empty relation, i.e. set() + if len(s) == 0: + return True + # all the elements are tuples of the same length + elif all(isinstance(el, tuple) for el in s) and len(max(s)) == len(min(s)): + return True + else: + raise ValueError("Set %r contains sequences of different lengths" % s) + + +def set2rel(s): + """ + Convert a set containing individuals (strings or numbers) into a set of + unary tuples. Any tuples of strings already in the set are passed through + unchanged. + + For example: + - set(['a', 'b']) => set([('a',), ('b',)]) + - set([3, 27]) => set([('3',), ('27',)]) + + :type s: set + :rtype: set of tuple of str + """ + new = set() + for elem in s: + if isinstance(elem, str): + new.add((elem,)) + elif isinstance(elem, int): + new.add(str(elem)) + else: + new.add(elem) + return new + + +def arity(rel): + """ + Check the arity of a relation. + :type rel: set of tuples + :rtype: int of tuple of str + """ + if len(rel) == 0: + return 0 + return len(list(rel)[0]) + + +class Valuation(dict): + """ + A dictionary which represents a model-theoretic Valuation of non-logical constants. + Keys are strings representing the constants to be interpreted, and values correspond + to individuals (represented as strings) and n-ary relations (represented as sets of tuples + of strings). + + An instance of ``Valuation`` will raise a KeyError exception (i.e., + just behave like a standard dictionary) if indexed with an expression that + is not in its list of symbols. + """ + + def __init__(self, xs): + """ + :param xs: a list of (symbol, value) pairs. + """ + super().__init__() + for sym, val in xs: + if isinstance(val, str) or isinstance(val, bool): + self[sym] = val + elif isinstance(val, set): + self[sym] = set2rel(val) + else: + msg = textwrap.fill( + "Error in initializing Valuation. " + "Unrecognized value for symbol '%s':\n%s" % (sym, val), + width=66, + ) + + raise ValueError(msg) + + def __getitem__(self, key): + if key in self: + return dict.__getitem__(self, key) + else: + raise Undefined("Unknown expression: '%s'" % key) + + def __str__(self): + return pformat(self) + + @property + def domain(self): + """Set-theoretic domain of the value-space of a Valuation.""" + dom = [] + for val in self.values(): + if isinstance(val, str): + dom.append(val) + elif not isinstance(val, bool): + dom.extend( + [elem for tuple_ in val for elem in tuple_ if elem is not None] + ) + return set(dom) + + @property + def symbols(self): + """The non-logical constants which the Valuation recognizes.""" + return sorted(self.keys()) + + @classmethod + def fromstring(cls, s): + return read_valuation(s) + + +########################################## +# REs used by the _read_valuation function +########################################## +# The ``(?`` re-scans the run +# from every position (match ``=+``, miss ``>``, backtrack), which is quadratic +# in the run length and lets a single valuation string pin a CPU core +# (CWE-1333; CVE-2026-12890). The lookbehind only lets ``=+`` start at the +# beginning of a run, so interior positions fail in O(1); the split result is +# unchanged. +_VAL_SPLIT_RE = re.compile(r"\s*(?\s*") +_ELEMENT_SPLIT_RE = re.compile(r"\s*,\s*") +_TUPLES_RE = re.compile( + r"""\s* + (\([^)]+\)) # tuple-expression + \s*""", + re.VERBOSE, +) + + +def _read_valuation_line(s): + """ + Read a line in a valuation file. + + Lines are expected to be of the form:: + + noosa => n + girl => {g1, g2} + chase => {(b1, g1), (b2, g1), (g1, d1), (g2, d2)} + + :param s: input line + :type s: str + :return: a pair (symbol, value) + :rtype: tuple + """ + pieces = _VAL_SPLIT_RE.split(s) + symbol = pieces[0] + value = pieces[1] + # check whether the value is meant to be a set + if value.startswith("{"): + value = value[1:-1] + tuple_strings = _TUPLES_RE.findall(value) + # are the set elements tuples? + if tuple_strings: + set_elements = [] + for ts in tuple_strings: + ts = ts[1:-1] + element = tuple(_ELEMENT_SPLIT_RE.split(ts)) + set_elements.append(element) + else: + set_elements = _ELEMENT_SPLIT_RE.split(value) + value = set(set_elements) + return symbol, value + + +def read_valuation(s, encoding=None): + """ + Convert a valuation string into a valuation. + + :param s: a valuation string + :type s: str + :param encoding: the encoding of the input string, if it is binary + :type encoding: str + :return: a ``nltk.sem`` valuation + :rtype: Valuation + """ + if encoding is not None: + s = s.decode(encoding) + statements = [] + for linenum, line in enumerate(s.splitlines()): + line = line.strip() + if line.startswith("#") or line == "": + continue + try: + statements.append(_read_valuation_line(line)) + except ValueError as e: + raise ValueError(f"Unable to parse line {linenum}: {line}") from e + return Valuation(statements) + + +class Assignment(dict): + r""" + A dictionary which represents an assignment of values to variables. + + An assignment can only assign values from its domain. + + If an unknown expression *a* is passed to a model *M*\ 's + interpretation function *i*, *i* will first check whether *M*\ 's + valuation assigns an interpretation to *a* as a constant, and if + this fails, *i* will delegate the interpretation of *a* to + *g*. *g* only assigns values to individual variables (i.e., + members of the class ``IndividualVariableExpression`` in the ``logic`` + module. If a variable is not assigned a value by *g*, it will raise + an ``Undefined`` exception. + + A variable *Assignment* is a mapping from individual variables to + entities in the domain. Individual variables are usually indicated + with the letters ``'x'``, ``'y'``, ``'w'`` and ``'z'``, optionally + followed by an integer (e.g., ``'x0'``, ``'y332'``). Assignments are + created using the ``Assignment`` constructor, which also takes the + domain as a parameter. + + >>> from nltk.sem.evaluate import Assignment + >>> dom = set(['u1', 'u2', 'u3', 'u4']) + >>> g3 = Assignment(dom, [('x', 'u1'), ('y', 'u2')]) + >>> g3 == {'x': 'u1', 'y': 'u2'} + True + + There is also a ``print`` format for assignments which uses a notation + closer to that in logic textbooks: + + >>> print(g3) + g[u1/x][u2/y] + + It is also possible to update an assignment using the ``add`` method: + + >>> dom = set(['u1', 'u2', 'u3', 'u4']) + >>> g4 = Assignment(dom) + >>> g4.add('x', 'u1') + {'x': 'u1'} + + With no arguments, ``purge()`` is equivalent to ``clear()`` on a dictionary: + + >>> g4.purge() + >>> g4 + {} + + :param domain: the domain of discourse + :type domain: set + :param assign: a list of (varname, value) associations + :type assign: list + """ + + def __init__(self, domain, assign=None): + super().__init__() + self.domain = domain + if assign: + for var, val in assign: + assert val in self.domain, "'{}' is not in the domain: {}".format( + val, + self.domain, + ) + assert is_indvar(var), ( + "Wrong format for an Individual Variable: '%s'" % var + ) + self[var] = val + self.variant = None + self._addvariant() + + def __getitem__(self, key): + if key in self: + return dict.__getitem__(self, key) + else: + raise Undefined("Not recognized as a variable: '%s'" % key) + + def copy(self): + new = Assignment(self.domain) + new.update(self) + return new + + def purge(self, var=None): + """ + Remove one or all keys (i.e. logic variables) from an + assignment, and update ``self.variant``. + + :param var: a Variable acting as a key for the assignment. + """ + if var: + del self[var] + else: + self.clear() + self._addvariant() + return None + + def __str__(self): + """ + Pretty printing for assignments. {'x', 'u'} appears as 'g[u/x]' + """ + gstring = "g" + # Deterministic output for unit testing. + variant = sorted(self.variant) + for val, var in variant: + gstring += f"[{val}/{var}]" + return gstring + + def _addvariant(self): + """ + Create a more pretty-printable version of the assignment. + """ + list_ = [] + for item in self.items(): + pair = (item[1], item[0]) + list_.append(pair) + self.variant = list_ + return None + + def add(self, var, val): + """ + Add a new variable-value pair to the assignment, and update + ``self.variant``. + + """ + assert val in self.domain, f"{val} is not in the domain {self.domain}" + assert is_indvar(var), "Wrong format for an Individual Variable: '%s'" % var + self[var] = val + self._addvariant() + return self + + +def _max_binder_depth(parsed): + """ + Greatest number of nested domain-quantifying binders along any path of + ``parsed`` (``all``/``exists``/``iota`` quantifiers and ``\\`` lambdas). + + ``Model.satisfy`` iterates the whole domain once per such binder, so a + nesting path of depth *k* explores O(|domain| ** k) domain-value + combinations; *k* is what bounds the blow-up, and it comes entirely from + the (untrusted) formula. Used by ``Model._check_satisfy_cost`` to refuse a + formula before the recursion explodes (CWE-770; CVE-2026-12840). + """ + if isinstance( + parsed, + (AllExpression, ExistsExpression, IotaExpression, LambdaExpression), + ): + return 1 + _max_binder_depth(parsed.term) + if isinstance(parsed, ApplicationExpression): + return max( + _max_binder_depth(parsed.function), + _max_binder_depth(parsed.argument), + ) + if isinstance(parsed, NegatedExpression): + return _max_binder_depth(parsed.term) + if isinstance( + parsed, + ( + AndExpression, + OrExpression, + ImpExpression, + IffExpression, + EqualityExpression, + ), + ): + return max( + _max_binder_depth(parsed.first), + _max_binder_depth(parsed.second), + ) + return 0 + + +class Model: + """ + A first order model is a domain *D* of discourse and a valuation *V*. + + A domain *D* is a set, and a valuation *V* is a map that associates + expressions with values in the model. + The domain of *V* should be a subset of *D*. + + Construct a new ``Model``. + + :type domain: set + :param domain: A set of entities representing the domain of discourse of the model. + :type valuation: Valuation + :param valuation: the valuation of the model. + :param prop: If this is set, then we are building a propositional\ + model and don't require the domain of *V* to be subset of *D*. + """ + + #: Upper bound on the number of domain-value combinations a single nested + #: quantifier/lambda path may explore. ``satisfy`` iterates the whole domain + #: once per nested binder, so a path of *k* nested binders explores + #: |domain| ** k combinations; *k* is attacker-controlled, and this product + #: is the term that blows up (a formula's total work is at most this times a + #: factor linear in the formula's size -- independent sibling quantifiers + #: only add, they do not multiply). A formula whose deepest path would + #: exceed this bound is refused up front instead of being allowed to exhaust + #: memory/CPU (CWE-770; CVE-2026-12840). + MAX_SATISFY_OPERATIONS = 1_000_000 + + def __init__(self, domain, valuation): + assert isinstance(domain, set) + self.domain = domain + self.valuation = valuation + if not domain.issuperset(valuation.domain): + raise Error( + "The valuation domain, %s, must be a subset of the model's domain, %s" + % (valuation.domain, domain) + ) + + def __repr__(self): + return f"({self.domain!r}, {self.valuation!r})" + + def __str__(self): + return f"Domain = {self.domain},\nValuation = \n{self.valuation}" + + def evaluate(self, expr, g, trace=None): + """ + Read input expressions, and provide a handler for ``satisfy`` + that blocks further propagation of the ``Undefined`` error. + :param expr: An ``Expression`` of ``logic``. + :type g: Assignment + :param g: an assignment to individual variables. + :rtype: bool or 'Undefined' + """ + try: + parsed = Expression.fromstring(expr) + value = self.satisfy(parsed, g, trace=trace) + if trace: + print() + print(f"'{expr}' evaluates to {value} under M, {g}") + return value + except Undefined: + if trace: + print() + print(f"'{expr}' is undefined under M, {g}") + return "Undefined" + + def satisfy(self, parsed, g, trace=None): + """ + Recursive interpretation function for a formula of first-order logic. + + Raises an ``Undefined`` error when ``parsed`` is an atomic string + but is not a symbol or an individual variable. + + :return: Returns a truth value or ``Undefined`` if ``parsed`` is\ + complex, and calls the interpretation function ``i`` if ``parsed``\ + is atomic. + + :param parsed: An expression of ``logic``. + :type g: Assignment + :param g: an assignment to individual variables. + :raise Error: if ``parsed`` has a nested quantifier/lambda path whose\ + domain-value combinations (|domain| ** nesting depth) would exceed\ + ``MAX_SATISFY_OPERATIONS`` (CWE-770). + """ + self._check_satisfy_cost(parsed) + return self._satisfy(parsed, g, trace) + + def _check_satisfy_cost(self, parsed): + """ + Refuse ``parsed`` if its most deeply nested quantifier/lambda path + would explore more than ``MAX_SATISFY_OPERATIONS`` domain-value + combinations, i.e. |domain| ** (max binder nesting depth). That product + is the combinatorial factor that drives the blow-up; bounding it + *before* the recursion runs stops a deeply-nested formula from + exhausting memory/CPU (CWE-770; CVE-2026-12840). Independent sibling + quantifiers are not multiplied (they only add a factor linear in the + formula's size), so they are intentionally not counted here. The check + itself is cheap (linear in the size of ``parsed``) and, by bailing on + the first overflow, uses no large integers. + """ + size = len(self.domain) + if size < 2: + # 0 or 1 domain element: |domain| ** k can never exceed the bound. + return + ops = 1 + for _ in range(_max_binder_depth(parsed)): + ops *= size + if ops > self.MAX_SATISFY_OPERATIONS: + raise Error( + "Refusing to evaluate formula: over a domain of %d " + "element(s) its nested quantifiers would explore more than " + "%d domain-value combinations (|domain| ** nesting depth) " + "(CWE-770). Reduce the formula's quantifier nesting or the " + "domain size." % (size, self.MAX_SATISFY_OPERATIONS) + ) + + def _satisfy(self, parsed, g, trace=None): + if isinstance(parsed, ApplicationExpression): + function, arguments = parsed.uncurry() + if isinstance(function, AbstractVariableExpression): + # It's a predicate expression ("P(x,y)"), so used uncurried arguments + funval = self._satisfy(function, g) + argvals = tuple(self._satisfy(arg, g) for arg in arguments) + return argvals in funval + else: + # It must be a lambda expression, so use curried form + funval = self._satisfy(parsed.function, g) + argval = self._satisfy(parsed.argument, g) + return funval[argval] + elif isinstance(parsed, NegatedExpression): + return not self._satisfy(parsed.term, g) + elif isinstance(parsed, AndExpression): + return self._satisfy(parsed.first, g) and self._satisfy(parsed.second, g) + elif isinstance(parsed, OrExpression): + return self._satisfy(parsed.first, g) or self._satisfy(parsed.second, g) + elif isinstance(parsed, ImpExpression): + return (not self._satisfy(parsed.first, g)) or self._satisfy( + parsed.second, g + ) + elif isinstance(parsed, IffExpression): + return self._satisfy(parsed.first, g) == self._satisfy(parsed.second, g) + elif isinstance(parsed, EqualityExpression): + return self._satisfy(parsed.first, g) == self._satisfy(parsed.second, g) + elif isinstance(parsed, AllExpression): + new_g = g.copy() + for u in self.domain: + new_g.add(parsed.variable.name, u) + if not self._satisfy(parsed.term, new_g): + return False + return True + elif isinstance(parsed, ExistsExpression): + new_g = g.copy() + for u in self.domain: + new_g.add(parsed.variable.name, u) + if self._satisfy(parsed.term, new_g): + return True + return False + elif isinstance(parsed, IotaExpression): + new_g = g.copy() + for u in self.domain: + new_g.add(parsed.variable.name, u) + if self._satisfy(parsed.term, new_g): + return True + return False + elif isinstance(parsed, LambdaExpression): + cf = {} + var = parsed.variable.name + for u in self.domain: + val = self._satisfy(parsed.term, g.add(var, u)) + # NB the dict would be a lot smaller if we do this: + # if val: cf[u] = val + # But then need to deal with cases where f(a) should yield + # a function rather than just False. + cf[u] = val + return cf + else: + return self.i(parsed, g, trace) + + # @decorator(trace_eval) + def i(self, parsed, g, trace=False): + """ + An interpretation function. + + Assuming that ``parsed`` is atomic: + + - if ``parsed`` is a non-logical constant, calls the valuation *V* + - else if ``parsed`` is an individual variable, calls assignment *g* + - else returns ``Undefined``. + + :param parsed: an ``Expression`` of ``logic``. + :type g: Assignment + :param g: an assignment to individual variables. + :return: a semantic value + """ + # If parsed is a propositional letter 'p', 'q', etc, it could be in valuation.symbols + # and also be an IndividualVariableExpression. We want to catch this first case. + # So there is a procedural consequence to the ordering of clauses here: + if parsed.variable.name in self.valuation.symbols: + return self.valuation[parsed.variable.name] + elif isinstance(parsed, IndividualVariableExpression): + return g[parsed.variable.name] + + else: + raise Undefined("Can't find a value for %s" % parsed) + + def satisfiers(self, parsed, varex, g, trace=None, nesting=0): + """ + Generate the entities from the model's domain that satisfy an open formula. + + :param parsed: an open formula + :type parsed: Expression + :param varex: the relevant free individual variable in ``parsed``. + :type varex: VariableExpression or str + :param g: a variable assignment + :type g: Assignment + :return: a set of the entities that satisfy ``parsed``. + """ + + spacer = " " + indent = spacer + (spacer * nesting) + candidates = [] + + if isinstance(varex, str): + var = Variable(varex) + else: + var = varex + + if var in parsed.free(): + if trace: + print() + print( + (spacer * nesting) + + f"Open formula is '{parsed}' with assignment {g}" + ) + for u in self.domain: + new_g = g.copy() + new_g.add(var.name, u) + if trace and trace > 1: + lowtrace = trace - 1 + else: + lowtrace = 0 + value = self.satisfy(parsed, new_g, lowtrace) + + if trace: + print(indent + "(trying assignment %s)" % new_g) + + # parsed == False under g[u/var]? + if not value: + if trace: + print(indent + f"value of '{parsed}' under {new_g} is False") + + # so g[u/var] is a satisfying assignment + else: + candidates.append(u) + if trace: + print(indent + f"value of '{parsed}' under {new_g} is {value}") + + result = {c for c in candidates} + # var isn't free in parsed + else: + raise Undefined(f"{var.name} is not free in {parsed}") + + return result + + +# ////////////////////////////////////////////////////////////////////// +# Demo.. +# ////////////////////////////////////////////////////////////////////// +# number of spacer chars +mult = 30 + + +# Demo 1: Propositional Logic +################# +def propdemo(trace=None): + """Example of a propositional model.""" + + global val1, dom1, m1, g1 + val1 = Valuation([("P", True), ("Q", True), ("R", False)]) + dom1 = set() + m1 = Model(dom1, val1) + g1 = Assignment(dom1) + + print() + print("*" * mult) + print("Propositional Formulas Demo") + print("*" * mult) + print("(Propositional constants treated as nullary predicates)") + print() + print("Model m1:\n", m1) + print("*" * mult) + sentences = [ + "(P & Q)", + "(P & R)", + "- P", + "- R", + "- - P", + "- (P & R)", + "(P | R)", + "(R | P)", + "(R | R)", + "(- P | R)", + "(P | - P)", + "(P -> Q)", + "(P -> R)", + "(R -> P)", + "(P <-> P)", + "(R <-> R)", + "(P <-> R)", + ] + + for sent in sentences: + if trace: + print() + m1.evaluate(sent, g1, trace) + else: + print(f"The value of '{sent}' is: {m1.evaluate(sent, g1)}") + + +# Demo 2: FOL Model +############# + + +def folmodel(quiet=False, trace=None): + """Example of a first-order model.""" + + global val2, v2, dom2, m2, g2 + + v2 = [ + ("adam", "b1"), + ("betty", "g1"), + ("fido", "d1"), + ("girl", {"g1", "g2"}), + ("boy", {"b1", "b2"}), + ("dog", {"d1"}), + ("love", {("b1", "g1"), ("b2", "g2"), ("g1", "b1"), ("g2", "b1")}), + ] + val2 = Valuation(v2) + dom2 = val2.domain + m2 = Model(dom2, val2) + g2 = Assignment(dom2, [("x", "b1"), ("y", "g2")]) + + if not quiet: + print() + print("*" * mult) + print("Models Demo") + print("*" * mult) + print("Model m2:\n", "-" * 14, "\n", m2) + print("Variable assignment = ", g2) + + exprs = ["adam", "boy", "love", "walks", "x", "y", "z"] + parsed_exprs = [Expression.fromstring(e) for e in exprs] + + print() + for parsed in parsed_exprs: + try: + print( + "The interpretation of '%s' in m2 is %s" + % (parsed, m2.i(parsed, g2)) + ) + except Undefined: + print("The interpretation of '%s' in m2 is Undefined" % parsed) + + applications = [ + ("boy", ("adam")), + ("walks", ("adam",)), + ("love", ("adam", "y")), + ("love", ("y", "adam")), + ] + + for fun, args in applications: + try: + funval = m2.i(Expression.fromstring(fun), g2) + argsval = tuple(m2.i(Expression.fromstring(arg), g2) for arg in args) + print(f"{fun}({args}) evaluates to {argsval in funval}") + except Undefined: + print(f"{fun}({args}) evaluates to Undefined") + + +# Demo 3: FOL +######### + + +def foldemo(trace=None): + """ + Interpretation of closed expressions in a first-order model. + """ + folmodel(quiet=True) + + print() + print("*" * mult) + print("FOL Formulas Demo") + print("*" * mult) + + formulas = [ + "love (adam, betty)", + "(adam = mia)", + "\\x. (boy(x) | girl(x))", + "\\x. boy(x)(adam)", + "\\x y. love(x, y)", + "\\x y. love(x, y)(adam)(betty)", + "\\x y. love(x, y)(adam, betty)", + "\\x y. (boy(x) & love(x, y))", + "\\x. exists y. (boy(x) & love(x, y))", + "exists z1. boy(z1)", + "exists x. (boy(x) & -(x = adam))", + "exists x. (boy(x) & all y. love(y, x))", + "all x. (boy(x) | girl(x))", + "all x. (girl(x) -> exists y. boy(y) & love(x, y))", # Every girl loves exists boy. + "exists x. (boy(x) & all y. (girl(y) -> love(y, x)))", # There is exists boy that every girl loves. + "exists x. (boy(x) & all y. (girl(y) -> love(x, y)))", # exists boy loves every girl. + "all x. (dog(x) -> - girl(x))", + "exists x. exists y. (love(x, y) & love(x, y))", + ] + + for fmla in formulas: + g2.purge() + if trace: + m2.evaluate(fmla, g2, trace) + else: + print(f"The value of '{fmla}' is: {m2.evaluate(fmla, g2)}") + + +# Demo 3: Satisfaction +############# + + +def satdemo(trace=None): + """Satisfiers of an open formula in a first order model.""" + + print() + print("*" * mult) + print("Satisfiers Demo") + print("*" * mult) + + folmodel(quiet=True) + + formulas = [ + "boy(x)", + "(x = x)", + "(boy(x) | girl(x))", + "(boy(x) & girl(x))", + "love(adam, x)", + "love(x, adam)", + "-(x = adam)", + "exists z22. love(x, z22)", + "exists y. love(y, x)", + "all y. (girl(y) -> love(x, y))", + "all y. (girl(y) -> love(y, x))", + "all y. (girl(y) -> (boy(x) & love(y, x)))", + "(boy(x) & all y. (girl(y) -> love(x, y)))", + "(boy(x) & all y. (girl(y) -> love(y, x)))", + "(boy(x) & exists y. (girl(y) & love(y, x)))", + "(girl(x) -> dog(x))", + "all y. (dog(y) -> (x = y))", + "exists y. love(y, x)", + "exists y. (love(adam, y) & love(y, x))", + ] + + if trace: + print(m2) + + for fmla in formulas: + print(fmla) + Expression.fromstring(fmla) + + parsed = [Expression.fromstring(fmla) for fmla in formulas] + + for p in parsed: + g2.purge() + print( + "The satisfiers of '{}' are: {}".format(p, m2.satisfiers(p, "x", g2, trace)) + ) + + +def demo(num=0, trace=None): + """ + Run exists demos. + + - num = 1: propositional logic demo + - num = 2: first order model demo (only if trace is set) + - num = 3: first order sentences demo + - num = 4: satisfaction of open formulas demo + - any other value: run all the demos + + :param trace: trace = 1, or trace = 2 for more verbose tracing + """ + demos = {1: propdemo, 2: folmodel, 3: foldemo, 4: satdemo} + + try: + demos[num](trace=trace) + except KeyError: + for num in demos: + demos[num](trace=trace) + + +if __name__ == "__main__": + demo(2, trace=0) diff --git a/nltk/sem/glue.py b/nltk/sem/glue.py new file mode 100644 index 0000000..124998d --- /dev/null +++ b/nltk/sem/glue.py @@ -0,0 +1,835 @@ +# Natural Language Toolkit: Glue Semantics +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +import os +from itertools import chain + +import nltk +from nltk.internals import Counter +from nltk.sem import drt, linearlogic +from nltk.sem.logic import ( + AbstractVariableExpression, + Expression, + LambdaExpression, + Variable, + VariableExpression, +) +from nltk.tag import BigramTagger, RegexpTagger, TrigramTagger, UnigramTagger + +SPEC_SEMTYPES = { + "a": "ex_quant", + "an": "ex_quant", + "every": "univ_quant", + "the": "def_art", + "no": "no_quant", + "default": "ex_quant", +} + +OPTIONAL_RELATIONSHIPS = ["nmod", "vmod", "punct"] + + +class GlueFormula: + def __init__(self, meaning, glue, indices=None): + if not indices: + indices = set() + + if isinstance(meaning, str): + self.meaning = Expression.fromstring(meaning) + elif isinstance(meaning, Expression): + self.meaning = meaning + else: + raise RuntimeError( + "Meaning term neither string or expression: %s, %s" + % (meaning, meaning.__class__) + ) + + if isinstance(glue, str): + self.glue = linearlogic.LinearLogicParser().parse(glue) + elif isinstance(glue, linearlogic.Expression): + self.glue = glue + else: + raise RuntimeError( + "Glue term neither string or expression: %s, %s" + % (glue, glue.__class__) + ) + + self.indices = indices + + def applyto(self, arg): + """self = (\\x.(walk x), (subj -o f)) + arg = (john , subj) + returns ((walk john), f) + """ + if self.indices & arg.indices: # if the sets are NOT disjoint + raise linearlogic.LinearLogicApplicationException( + f"'{self}' applied to '{arg}'. Indices are not disjoint." + ) + else: # if the sets ARE disjoint + return_indices = self.indices | arg.indices + + try: + return_glue = linearlogic.ApplicationExpression( + self.glue, arg.glue, arg.indices + ) + except linearlogic.LinearLogicApplicationException as e: + raise linearlogic.LinearLogicApplicationException( + f"'{self.simplify()}' applied to '{arg.simplify()}'" + ) from e + + arg_meaning_abstracted = arg.meaning + if return_indices: + for dep in self.glue.simplify().antecedent.dependencies[ + ::-1 + ]: # if self.glue is (A -o B), dep is in A.dependencies + arg_meaning_abstracted = self.make_LambdaExpression( + Variable("v%s" % dep), arg_meaning_abstracted + ) + return_meaning = self.meaning.applyto(arg_meaning_abstracted) + + return self.__class__(return_meaning, return_glue, return_indices) + + def make_VariableExpression(self, name): + return VariableExpression(name) + + def make_LambdaExpression(self, variable, term): + return LambdaExpression(variable, term) + + def lambda_abstract(self, other): + assert isinstance(other, GlueFormula) + assert isinstance(other.meaning, AbstractVariableExpression) + return self.__class__( + self.make_LambdaExpression(other.meaning.variable, self.meaning), + linearlogic.ImpExpression(other.glue, self.glue), + ) + + def compile(self, counter=None): + """From Iddo Lev's PhD Dissertation p108-109""" + if not counter: + counter = Counter() + (compiled_glue, new_forms) = self.glue.simplify().compile_pos( + counter, self.__class__ + ) + return new_forms + [ + self.__class__(self.meaning, compiled_glue, {counter.get()}) + ] + + def simplify(self): + return self.__class__( + self.meaning.simplify(), self.glue.simplify(), self.indices + ) + + def __eq__(self, other): + return ( + self.__class__ == other.__class__ + and self.meaning == other.meaning + and self.glue == other.glue + ) + + def __ne__(self, other): + return not self == other + + # sorting for use in doctests which must be deterministic + def __lt__(self, other): + return str(self) < str(other) + + def __str__(self): + assert isinstance(self.indices, set) + accum = f"{self.meaning} : {self.glue}" + if self.indices: + accum += ( + " : {" + ", ".join(str(index) for index in sorted(self.indices)) + "}" + ) + return accum + + def __repr__(self): + return "%s" % self + + +class GlueDict(dict): + def __init__(self, filename, encoding=None): + self.filename = filename + self.file_encoding = encoding + self.read_file() + + def read_file(self, empty_first=True): + if empty_first: + self.clear() + + try: + contents = nltk.data.load( + self.filename, format="text", encoding=self.file_encoding + ) + # TODO: the above can't handle zip files, but this should anyway be fixed in nltk.data.load() + except LookupError as e: + try: + contents = nltk.data.load( + "file:" + self.filename, format="text", encoding=self.file_encoding + ) + except LookupError: + raise e + lines = contents.splitlines() + + for line in lines: # example: 'n : (\\x.( x), (v-or))' + # lambdacalc -^ linear logic -^ + line = line.strip() # remove trailing newline + if not len(line): + continue # skip empty lines + if line[0] == "#": + continue # skip commented out lines + + parts = line.split( + " : ", 2 + ) # ['verb', '(\\x.( x), ( subj -o f ))', '[subj]'] + + glue_formulas = [] + paren_count = 0 + tuple_start = 0 + tuple_comma = 0 + + relationships = None + + if len(parts) > 1: + for i, c in enumerate(parts[1]): + if c == "(": + if paren_count == 0: # if it's the first '(' of a tuple + tuple_start = i + 1 # then save the index + paren_count += 1 + elif c == ")": + paren_count -= 1 + if paren_count == 0: # if it's the last ')' of a tuple + meaning_term = parts[1][ + tuple_start:tuple_comma + ] # '\\x.( x)' + glue_term = parts[1][tuple_comma + 1 : i] # '(v-r)' + glue_formulas.append( + [meaning_term, glue_term] + ) # add the GlueFormula to the list + elif c == ",": + if ( + paren_count == 1 + ): # if it's a comma separating the parts of the tuple + tuple_comma = i # then save the index + elif c == "#": # skip comments at the ends of lines + if ( + paren_count != 0 + ): # if the line hasn't parsed correctly so far + raise RuntimeError( + "Formula syntax is incorrect for entry " + line + ) + break # break to the next line + + if len(parts) > 2: # if there is a relationship entry at the end + rel_start = parts[2].index("[") + 1 + rel_end = parts[2].index("]") + if rel_start == rel_end: + relationships = frozenset() + else: + relationships = frozenset( + r.strip() for r in parts[2][rel_start:rel_end].split(",") + ) + + try: + start_inheritance = parts[0].index("(") + end_inheritance = parts[0].index(")") + sem = parts[0][:start_inheritance].strip() + supertype = parts[0][start_inheritance + 1 : end_inheritance] + except Exception: + sem = parts[0].strip() + supertype = None + + if sem not in self: + self[sem] = {} + + if ( + relationships is None + ): # if not specified for a specific relationship set + # add all relationship entries for parents + if supertype: + for rels in self[supertype]: + if rels not in self[sem]: + self[sem][rels] = [] + glue = self[supertype][rels] + self[sem][rels].extend(glue) + self[sem][rels].extend( + glue_formulas + ) # add the glue formulas to every rel entry + else: + if None not in self[sem]: + self[sem][None] = [] + self[sem][None].extend( + glue_formulas + ) # add the glue formulas to every rel entry + else: + if relationships not in self[sem]: + self[sem][relationships] = [] + if supertype: + self[sem][relationships].extend(self[supertype][relationships]) + self[sem][relationships].extend( + glue_formulas + ) # add the glue entry to the dictionary + + def __str__(self): + accum = "" + for pos in self: + str_pos = "%s" % pos + for relset in self[pos]: + i = 1 + for gf in self[pos][relset]: + if i == 1: + accum += str_pos + ": " + else: + accum += " " * (len(str_pos) + 2) + accum += "%s" % gf + if relset and i == len(self[pos][relset]): + accum += " : %s" % relset + accum += "\n" + i += 1 + return accum + + def to_glueformula_list(self, depgraph, node=None, counter=None, verbose=False): + if node is None: + # TODO: should it be depgraph.root? Is this code tested? + top = depgraph.nodes[0] + depList = list(chain.from_iterable(top["deps"].values())) + root = depgraph.nodes[depList[0]] + + return self.to_glueformula_list(depgraph, root, Counter(), verbose) + + glueformulas = self.lookup(node, depgraph, counter) + for dep_idx in chain.from_iterable(node["deps"].values()): + dep = depgraph.nodes[dep_idx] + glueformulas.extend( + self.to_glueformula_list(depgraph, dep, counter, verbose) + ) + return glueformulas + + def lookup(self, node, depgraph, counter): + semtype_names = self.get_semtypes(node) + + semtype = None + for name in semtype_names: + if name in self: + semtype = self[name] + break + if semtype is None: + # raise KeyError, "There is no GlueDict entry for sem type '%s' (for '%s')" % (sem, word) + return [] + + self.add_missing_dependencies(node, depgraph) + + lookup = self._lookup_semtype_option(semtype, node, depgraph) + + if not len(lookup): + raise KeyError( + "There is no GlueDict entry for sem type of '%s' " + "with tag '%s', and rel '%s'" % (node["word"], node["tag"], node["rel"]) + ) + + return self.get_glueformulas_from_semtype_entry( + lookup, node["word"], node, depgraph, counter + ) + + def add_missing_dependencies(self, node, depgraph): + rel = node["rel"].lower() + + if rel == "main": + headnode = depgraph.nodes[node["head"]] + subj = self.lookup_unique("subj", headnode, depgraph) + relation = subj["rel"] + node["deps"].setdefault(relation, []) + node["deps"][relation].append(subj["address"]) + # node['deps'].append(subj['address']) + + def _lookup_semtype_option(self, semtype, node, depgraph): + relationships = frozenset( + depgraph.nodes[dep]["rel"].lower() + for dep in chain.from_iterable(node["deps"].values()) + if depgraph.nodes[dep]["rel"].lower() not in OPTIONAL_RELATIONSHIPS + ) + + try: + lookup = semtype[relationships] + except KeyError: + # An exact match is not found, so find the best match where + # 'best' is defined as the glue entry whose relationship set has the + # most relations of any possible relationship set that is a subset + # of the actual depgraph + best_match = frozenset() + for relset_option in set(semtype) - {None}: + if ( + len(relset_option) > len(best_match) + and relset_option < relationships + ): + best_match = relset_option + if not best_match: + if None in semtype: + best_match = None + else: + return None + lookup = semtype[best_match] + + return lookup + + def get_semtypes(self, node): + """ + Based on the node, return a list of plausible semtypes in order of + plausibility. + """ + rel = node["rel"].lower() + word = node["word"].lower() + + if rel == "spec": + if word in SPEC_SEMTYPES: + return [SPEC_SEMTYPES[word]] + else: + return [SPEC_SEMTYPES["default"]] + elif rel in ["nmod", "vmod"]: + return [node["tag"], rel] + else: + return [node["tag"]] + + def get_glueformulas_from_semtype_entry( + self, lookup, word, node, depgraph, counter + ): + glueformulas = [] + + glueFormulaFactory = self.get_GlueFormula_factory() + for meaning, glue in lookup: + gf = glueFormulaFactory(self.get_meaning_formula(meaning, word), glue) + if not len(glueformulas): + gf.word = word + else: + gf.word = f"{word}{len(glueformulas) + 1}" + + gf.glue = self.initialize_labels(gf.glue, node, depgraph, counter.get()) + + glueformulas.append(gf) + return glueformulas + + def get_meaning_formula(self, generic, word): + """ + :param generic: A meaning formula string containing the + parameter "" + :param word: The actual word to be replace "" + """ + word = word.replace(".", "") + return generic.replace("", word) + + def initialize_labels(self, expr, node, depgraph, unique_index): + if isinstance(expr, linearlogic.AtomicExpression): + name = self.find_label_name(expr.name.lower(), node, depgraph, unique_index) + if name[0].isupper(): + return linearlogic.VariableExpression(name) + else: + return linearlogic.ConstantExpression(name) + else: + return linearlogic.ImpExpression( + self.initialize_labels(expr.antecedent, node, depgraph, unique_index), + self.initialize_labels(expr.consequent, node, depgraph, unique_index), + ) + + def find_label_name(self, name, node, depgraph, unique_index): + try: + dot = name.index(".") + + before_dot = name[:dot] + after_dot = name[dot + 1 :] + if before_dot == "super": + return self.find_label_name( + after_dot, depgraph.nodes[node["head"]], depgraph, unique_index + ) + else: + return self.find_label_name( + after_dot, + self.lookup_unique(before_dot, node, depgraph), + depgraph, + unique_index, + ) + except ValueError: + lbl = self.get_label(node) + if name == "f": + return lbl + elif name == "v": + return "%sv" % lbl + elif name == "r": + return "%sr" % lbl + elif name == "super": + return self.get_label(depgraph.nodes[node["head"]]) + elif name == "var": + return f"{lbl.upper()}{unique_index}" + elif name == "a": + return self.get_label(self.lookup_unique("conja", node, depgraph)) + elif name == "b": + return self.get_label(self.lookup_unique("conjb", node, depgraph)) + else: + return self.get_label(self.lookup_unique(name, node, depgraph)) + + def get_label(self, node): + """ + Pick an alphabetic character as identifier for an entity in the model. + + :param value: where to index into the list of characters + :type value: int + """ + value = node["address"] + + letter = [ + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + ][value - 1] + num = int(value) // 26 + if num > 0: + return letter + str(num) + else: + return letter + + def lookup_unique(self, rel, node, depgraph): + """ + Lookup 'key'. There should be exactly one item in the associated relation. + """ + deps = [ + depgraph.nodes[dep] + for dep in chain.from_iterable(node["deps"].values()) + if depgraph.nodes[dep]["rel"].lower() == rel.lower() + ] + + if len(deps) == 0: + raise KeyError( + "'{}' doesn't contain a feature '{}'".format(node["word"], rel) + ) + elif len(deps) > 1: + raise KeyError( + "'{}' should only have one feature '{}'".format(node["word"], rel) + ) + else: + return deps[0] + + def get_GlueFormula_factory(self): + return GlueFormula + + +class Glue: + def __init__( + self, semtype_file=None, remove_duplicates=False, depparser=None, verbose=False + ): + self.verbose = verbose + self.remove_duplicates = remove_duplicates + self.depparser = depparser + + from nltk import Prover9 + + self.prover = Prover9() + + if semtype_file: + self.semtype_file = semtype_file + else: + self.semtype_file = os.path.join( + "grammars", "sample_grammars", "glue.semtype" + ) + + def train_depparser(self, depgraphs=None): + if depgraphs: + self.depparser.train(depgraphs) + else: + self.depparser.train_from_file( + nltk.data.find( + os.path.join("grammars", "sample_grammars", "glue_train.conll") + ) + ) + + def parse_to_meaning(self, sentence): + readings = [] + for agenda in self.parse_to_compiled(sentence): + readings.extend(self.get_readings(agenda)) + return readings + + def get_readings(self, agenda): + readings = [] + agenda_length = len(agenda) + atomics = dict() + nonatomics = dict() + while agenda: # is not empty + cur = agenda.pop() + glue_simp = cur.glue.simplify() + if isinstance( + glue_simp, linearlogic.ImpExpression + ): # if cur.glue is non-atomic + for key in atomics: + try: + if isinstance(cur.glue, linearlogic.ApplicationExpression): + bindings = cur.glue.bindings + else: + bindings = linearlogic.BindingDict() + glue_simp.antecedent.unify(key, bindings) + for atomic in atomics[key]: + if not ( + cur.indices & atomic.indices + ): # if the sets of indices are disjoint + try: + agenda.append(cur.applyto(atomic)) + except linearlogic.LinearLogicApplicationException: + pass + except linearlogic.UnificationException: + pass + try: + nonatomics[glue_simp.antecedent].append(cur) + except KeyError: + nonatomics[glue_simp.antecedent] = [cur] + + else: # else cur.glue is atomic + for key in nonatomics: + for nonatomic in nonatomics[key]: + try: + if isinstance( + nonatomic.glue, linearlogic.ApplicationExpression + ): + bindings = nonatomic.glue.bindings + else: + bindings = linearlogic.BindingDict() + glue_simp.unify(key, bindings) + if not ( + cur.indices & nonatomic.indices + ): # if the sets of indices are disjoint + try: + agenda.append(nonatomic.applyto(cur)) + except linearlogic.LinearLogicApplicationException: + pass + except linearlogic.UnificationException: + pass + try: + atomics[glue_simp].append(cur) + except KeyError: + atomics[glue_simp] = [cur] + + for entry in atomics: + for gf in atomics[entry]: + if len(gf.indices) == agenda_length: + self._add_to_reading_list(gf, readings) + for entry in nonatomics: + for gf in nonatomics[entry]: + if len(gf.indices) == agenda_length: + self._add_to_reading_list(gf, readings) + return readings + + def _add_to_reading_list(self, glueformula, reading_list): + add_reading = True + if self.remove_duplicates: + for reading in reading_list: + try: + if reading.equiv(glueformula.meaning, self.prover): + add_reading = False + break + except Exception as e: + # if there is an exception, the syntax of the formula + # may not be understandable by the prover, so don't + # throw out the reading. + print("Error when checking logical equality of statements", e) + + if add_reading: + reading_list.append(glueformula.meaning) + + def parse_to_compiled(self, sentence): + gfls = [self.depgraph_to_glue(dg) for dg in self.dep_parse(sentence)] + return [self.gfl_to_compiled(gfl) for gfl in gfls] + + def dep_parse(self, sentence): + """ + Return a dependency graph for the sentence. + + :param sentence: the sentence to be parsed + :type sentence: list(str) + :rtype: DependencyGraph + """ + + # Lazy-initialize the depparser + if self.depparser is None: + from nltk.parse import MaltParser + + self.depparser = MaltParser(tagger=self.get_pos_tagger()) + if not self.depparser._trained: + self.train_depparser() + return self.depparser.parse(sentence, verbose=self.verbose) + + def depgraph_to_glue(self, depgraph): + return self.get_glue_dict().to_glueformula_list(depgraph) + + def get_glue_dict(self): + return GlueDict(self.semtype_file) + + def gfl_to_compiled(self, gfl): + index_counter = Counter() + return_list = [] + for gf in gfl: + return_list.extend(gf.compile(index_counter)) + + if self.verbose: + print("Compiled Glue Premises:") + for cgf in return_list: + print(cgf) + + return return_list + + def get_pos_tagger(self): + from nltk.corpus import brown + + regexp_tagger = RegexpTagger( + [ + (r"^-?[0-9]+(\.[0-9]+)?$", "CD"), # cardinal numbers + (r"(The|the|A|a|An|an)$", "AT"), # articles + (r".*able$", "JJ"), # adjectives + (r".*ness$", "NN"), # nouns formed from adjectives + (r".*ly$", "RB"), # adverbs + (r".*s$", "NNS"), # plural nouns + (r".*ing$", "VBG"), # gerunds + (r".*ed$", "VBD"), # past tense verbs + (r".*", "NN"), # nouns (default) + ] + ) + brown_train = brown.tagged_sents(categories="news") + unigram_tagger = UnigramTagger(brown_train, backoff=regexp_tagger) + bigram_tagger = BigramTagger(brown_train, backoff=unigram_tagger) + trigram_tagger = TrigramTagger(brown_train, backoff=bigram_tagger) + + # Override particular words + main_tagger = RegexpTagger( + [(r"(A|a|An|an)$", "ex_quant"), (r"(Every|every|All|all)$", "univ_quant")], + backoff=trigram_tagger, + ) + + return main_tagger + + +class DrtGlueFormula(GlueFormula): + def __init__(self, meaning, glue, indices=None): + if not indices: + indices = set() + + if isinstance(meaning, str): + self.meaning = drt.DrtExpression.fromstring(meaning) + elif isinstance(meaning, drt.DrtExpression): + self.meaning = meaning + else: + raise RuntimeError( + "Meaning term neither string or expression: %s, %s" + % (meaning, meaning.__class__) + ) + + if isinstance(glue, str): + self.glue = linearlogic.LinearLogicParser().parse(glue) + elif isinstance(glue, linearlogic.Expression): + self.glue = glue + else: + raise RuntimeError( + "Glue term neither string or expression: %s, %s" + % (glue, glue.__class__) + ) + + self.indices = indices + + def make_VariableExpression(self, name): + return drt.DrtVariableExpression(name) + + def make_LambdaExpression(self, variable, term): + return drt.DrtLambdaExpression(variable, term) + + +class DrtGlueDict(GlueDict): + def get_GlueFormula_factory(self): + return DrtGlueFormula + + +class DrtGlue(Glue): + def __init__( + self, semtype_file=None, remove_duplicates=False, depparser=None, verbose=False + ): + if not semtype_file: + semtype_file = os.path.join( + "grammars", "sample_grammars", "drt_glue.semtype" + ) + Glue.__init__(self, semtype_file, remove_duplicates, depparser, verbose) + + def get_glue_dict(self): + return DrtGlueDict(self.semtype_file) + + +def demo(show_example=-1): + from nltk.parse import MaltParser + + examples = [ + "David sees Mary", + "David eats a sandwich", + "every man chases a dog", + "every man believes a dog sleeps", + "John gives David a sandwich", + "John chases himself", + ] + # 'John persuades David to order a pizza', + # 'John tries to go', + # 'John tries to find a unicorn', + # 'John seems to vanish', + # 'a unicorn seems to approach', + # 'every big cat leaves', + # 'every gray cat leaves', + # 'every big gray cat leaves', + # 'a former senator leaves', + + print("============== DEMO ==============") + + tagger = RegexpTagger( + [ + ("^(David|Mary|John)$", "NNP"), + ( + "^(sees|eats|chases|believes|gives|sleeps|chases|persuades|tries|seems|leaves)$", + "VB", + ), + ("^(go|order|vanish|find|approach)$", "VB"), + ("^(a)$", "ex_quant"), + ("^(every)$", "univ_quant"), + ("^(sandwich|man|dog|pizza|unicorn|cat|senator)$", "NN"), + ("^(big|gray|former)$", "JJ"), + ("^(him|himself)$", "PRP"), + ] + ) + + depparser = MaltParser(tagger=tagger) + glue = Glue(depparser=depparser, verbose=False) + + for i, sentence in enumerate(examples): + if i == show_example or show_example == -1: + print(f"[[[Example {i}]]] {sentence}") + for reading in glue.parse_to_meaning(sentence.split()): + print(reading.simplify()) + print("") + + +if __name__ == "__main__": + demo() diff --git a/nltk/sem/hole.py b/nltk/sem/hole.py new file mode 100644 index 0000000..121e9bf --- /dev/null +++ b/nltk/sem/hole.py @@ -0,0 +1,395 @@ +# Natural Language Toolkit: Logic +# +# Author: Peter Wang +# Updated by: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +An implementation of the Hole Semantics model, following Blackburn and Bos, +Representation and Inference for Natural Language (CSLI, 2005). + +The semantic representations are built by the grammar hole.fcfg. +This module contains driver code to read in sentences and parse them +according to a hole semantics grammar. + +After parsing, the semantic representation is in the form of an underspecified +representation that is not easy to read. We use a "plugging" algorithm to +convert that representation into first-order logic formulas. +""" + +from functools import reduce + +from nltk.parse import load_parser +from nltk.sem.logic import ( + AllExpression, + AndExpression, + ApplicationExpression, + ExistsExpression, + IffExpression, + ImpExpression, + LambdaExpression, + NegatedExpression, + OrExpression, +) +from nltk.sem.skolemize import skolemize + +# Note that in this code there may be multiple types of trees being referred to: +# +# 1. parse trees +# 2. the underspecified representation +# 3. first-order logic formula trees +# 4. the search space when plugging (search tree) +# + + +class Constants: + ALL = "ALL" + EXISTS = "EXISTS" + NOT = "NOT" + AND = "AND" + OR = "OR" + IMP = "IMP" + IFF = "IFF" + PRED = "PRED" + LEQ = "LEQ" + HOLE = "HOLE" + LABEL = "LABEL" + + MAP = { + ALL: lambda v, e: AllExpression(v.variable, e), + EXISTS: lambda v, e: ExistsExpression(v.variable, e), + NOT: NegatedExpression, + AND: AndExpression, + OR: OrExpression, + IMP: ImpExpression, + IFF: IffExpression, + PRED: ApplicationExpression, + } + + +class HoleSemantics: + """ + This class holds the broken-down components of a hole semantics, i.e. it + extracts the holes, labels, logic formula fragments and constraints out of + a big conjunction of such as produced by the hole semantics grammar. It + then provides some operations on the semantics dealing with holes, labels + and finding legal ways to plug holes with labels. + """ + + def __init__(self, usr): + """ + Constructor. `usr' is a ``sem.Expression`` representing an + Underspecified Representation Structure (USR). A USR has the following + special predicates: + ALL(l,v,n), + EXISTS(l,v,n), + AND(l,n,n), + OR(l,n,n), + IMP(l,n,n), + IFF(l,n,n), + PRED(l,v,n,v[,v]*) where the brackets and star indicate zero or more repetitions, + LEQ(n,n), + HOLE(n), + LABEL(n) + where l is the label of the node described by the predicate, n is either + a label or a hole, and v is a variable. + """ + self.holes = set() + self.labels = set() + self.fragments = {} # mapping of label -> formula fragment + self.constraints = set() # set of Constraints + self._break_down(usr) + self.top_most_labels = self._find_top_most_labels() + self.top_hole = self._find_top_hole() + + def is_node(self, x): + """ + Return true if x is a node (label or hole) in this semantic + representation. + """ + return x in (self.labels | self.holes) + + def _break_down(self, usr): + """ + Extract holes, labels, formula fragments and constraints from the hole + semantics underspecified representation (USR). + """ + if isinstance(usr, AndExpression): + self._break_down(usr.first) + self._break_down(usr.second) + elif isinstance(usr, ApplicationExpression): + func, args = usr.uncurry() + if func.variable.name == Constants.LEQ: + self.constraints.add(Constraint(args[0], args[1])) + elif func.variable.name == Constants.HOLE: + self.holes.add(args[0]) + elif func.variable.name == Constants.LABEL: + self.labels.add(args[0]) + else: + label = args[0] + assert label not in self.fragments + self.fragments[label] = (func, args[1:]) + else: + raise ValueError(usr.label()) + + def _find_top_nodes(self, node_list): + top_nodes = node_list.copy() + for f in self.fragments.values(): + # the label is the first argument of the predicate + args = f[1] + for arg in args: + if arg in node_list: + top_nodes.discard(arg) + return top_nodes + + def _find_top_most_labels(self): + """ + Return the set of labels which are not referenced directly as part of + another formula fragment. These will be the top-most labels for the + subtree that they are part of. + """ + return self._find_top_nodes(self.labels) + + def _find_top_hole(self): + """ + Return the hole that will be the top of the formula tree. + """ + top_holes = self._find_top_nodes(self.holes) + assert len(top_holes) == 1 # it must be unique + return top_holes.pop() + + def pluggings(self): + """ + Calculate and return all the legal pluggings (mappings of labels to + holes) of this semantics given the constraints. + """ + record = [] + self._plug_nodes([(self.top_hole, [])], self.top_most_labels, {}, record) + return record + + def _plug_nodes(self, queue, potential_labels, plug_acc, record): + """ + Plug the nodes in `queue' with the labels in `potential_labels'. + + Each element of `queue' is a tuple of the node to plug and the list of + ancestor holes from the root of the graph to that node. + + `potential_labels' is a set of the labels which are still available for + plugging. + + `plug_acc' is the incomplete mapping of holes to labels made on the + current branch of the search tree so far. + + `record' is a list of all the complete pluggings that we have found in + total so far. It is the only parameter that is destructively updated. + """ + if queue != []: + (node, ancestors) = queue[0] + if node in self.holes: + # The node is a hole, try to plug it. + self._plug_hole( + node, ancestors, queue[1:], potential_labels, plug_acc, record + ) + else: + assert node in self.labels + # The node is a label. Replace it in the queue by the holes and + # labels in the formula fragment named by that label. + args = self.fragments[node][1] + head = [(a, ancestors) for a in args if self.is_node(a)] + self._plug_nodes(head + queue[1:], potential_labels, plug_acc, record) + else: + raise Exception("queue empty") + + def _plug_hole(self, hole, ancestors0, queue, potential_labels0, plug_acc0, record): + """ + Try all possible ways of plugging a single hole. + See _plug_nodes for the meanings of the parameters. + """ + # Add the current hole we're trying to plug into the list of ancestors. + assert hole not in ancestors0 + ancestors = [hole] + ancestors0 + + # Try each potential label in this hole in turn. + for l in potential_labels0: + # Is the label valid in this hole? + if self._violates_constraints(l, ancestors): + continue + + plug_acc = plug_acc0.copy() + plug_acc[hole] = l + potential_labels = potential_labels0.copy() + potential_labels.remove(l) + + if len(potential_labels) == 0: + # No more potential labels. That must mean all the holes have + # been filled so we have found a legal plugging so remember it. + # + # Note that the queue might not be empty because there might + # be labels on there that point to formula fragments with + # no holes in them. _sanity_check_plugging will make sure + # all holes are filled. + self._sanity_check_plugging(plug_acc, self.top_hole, []) + record.append(plug_acc) + else: + # Recursively try to fill in the rest of the holes in the + # queue. The label we just plugged into the hole could have + # holes of its own so at the end of the queue. Putting it on + # the end of the queue gives us a breadth-first search, so that + # all the holes at level i of the formula tree are filled + # before filling level i+1. + # A depth-first search would work as well since the trees must + # be finite but the bookkeeping would be harder. + self._plug_nodes( + queue + [(l, ancestors)], potential_labels, plug_acc, record + ) + + def _violates_constraints(self, label, ancestors): + """ + Return True if the `label' cannot be placed underneath the holes given + by the set `ancestors' because it would violate the constraints imposed + on it. + """ + for c in self.constraints: + if c.lhs == label: + if c.rhs not in ancestors: + return True + return False + + def _sanity_check_plugging(self, plugging, node, ancestors): + """ + Make sure that a given plugging is legal. We recursively go through + each node and make sure that no constraints are violated. + We also check that all holes have been filled. + """ + if node in self.holes: + ancestors = [node] + ancestors + label = plugging[node] + else: + label = node + assert label in self.labels + for c in self.constraints: + if c.lhs == label: + assert c.rhs in ancestors + args = self.fragments[label][1] + for arg in args: + if self.is_node(arg): + self._sanity_check_plugging(plugging, arg, [label] + ancestors) + + def formula_tree(self, plugging): + """ + Return the first-order logic formula tree for this underspecified + representation using the plugging given. + """ + return self._formula_tree(plugging, self.top_hole) + + def _formula_tree(self, plugging, node): + if node in plugging: + return self._formula_tree(plugging, plugging[node]) + elif node in self.fragments: + pred, args = self.fragments[node] + children = [self._formula_tree(plugging, arg) for arg in args] + return reduce(Constants.MAP[pred.variable.name], children) + else: + return node + + +class Constraint: + """ + This class represents a constraint of the form (L =< N), + where L is a label and N is a node (a label or a hole). + """ + + def __init__(self, lhs, rhs): + self.lhs = lhs + self.rhs = rhs + + def __eq__(self, other): + if self.__class__ == other.__class__: + return self.lhs == other.lhs and self.rhs == other.rhs + else: + return False + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + return hash(repr(self)) + + def __repr__(self): + return f"({self.lhs} < {self.rhs})" + + +def hole_readings(sentence, grammar_filename=None, verbose=False): + if not grammar_filename: + grammar_filename = "grammars/sample_grammars/hole.fcfg" + + if verbose: + print("Reading grammar file", grammar_filename) + + parser = load_parser(grammar_filename) + + # Parse the sentence. + tokens = sentence.split() + trees = list(parser.parse(tokens)) + if verbose: + print("Got %d different parses" % len(trees)) + + all_readings = [] + for tree in trees: + # Get the semantic feature from the top of the parse tree. + sem = tree.label()["SEM"].simplify() + + # Print the raw semantic representation. + if verbose: + print("Raw: ", sem) + + # Skolemize away all quantifiers. All variables become unique. + while isinstance(sem, LambdaExpression): + sem = sem.term + skolemized = skolemize(sem) + + if verbose: + print("Skolemized:", skolemized) + + # Break the hole semantics representation down into its components + # i.e. holes, labels, formula fragments and constraints. + hole_sem = HoleSemantics(skolemized) + + # Maybe show the details of the semantic representation. + if verbose: + print("Holes: ", hole_sem.holes) + print("Labels: ", hole_sem.labels) + print("Constraints: ", hole_sem.constraints) + print("Top hole: ", hole_sem.top_hole) + print("Top labels: ", hole_sem.top_most_labels) + print("Fragments:") + for l, f in hole_sem.fragments.items(): + print(f"\t{l}: {f}") + + # Find all the possible ways to plug the formulas together. + pluggings = hole_sem.pluggings() + + # Build FOL formula trees using the pluggings. + readings = list(map(hole_sem.formula_tree, pluggings)) + + # Print out the formulas in a textual format. + if verbose: + for i, r in enumerate(readings): + print() + print("%d. %s" % (i, r)) + print() + + all_readings.extend(readings) + + return all_readings + + +if __name__ == "__main__": + for r in hole_readings("a dog barks"): + print(r) + print() + for r in hole_readings("every girl chases a dog"): + print(r) diff --git a/nltk/sem/lfg.py b/nltk/sem/lfg.py new file mode 100644 index 0000000..55411ba --- /dev/null +++ b/nltk/sem/lfg.py @@ -0,0 +1,261 @@ +# Natural Language Toolkit: Lexical Functional Grammar +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +from itertools import chain + +from nltk.internals import Counter + + +class FStructure(dict): + def safeappend(self, key, item): + """ + Append 'item' to the list at 'key'. If no list exists for 'key', then + construct one. + """ + if key not in self: + self[key] = [] + self[key].append(item) + + def __setitem__(self, key, value): + dict.__setitem__(self, key.lower(), value) + + def __getitem__(self, key): + return dict.__getitem__(self, key.lower()) + + def __contains__(self, key): + return dict.__contains__(self, key.lower()) + + def to_glueformula_list(self, glue_dict): + depgraph = self.to_depgraph() + return glue_dict.to_glueformula_list(depgraph) + + def to_depgraph(self, rel=None): + from nltk.parse.dependencygraph import DependencyGraph + + depgraph = DependencyGraph() + nodes = depgraph.nodes + + self._to_depgraph(nodes, 0, "ROOT") + + # Add all the dependencies for all the nodes + for address, node in nodes.items(): + for n2 in (n for n in nodes.values() if n["rel"] != "TOP"): + if n2["head"] == address: + relation = n2["rel"] + node["deps"].setdefault(relation, []) + node["deps"][relation].append(n2["address"]) + + depgraph.root = nodes[1] + + return depgraph + + def _to_depgraph(self, nodes, head, rel): + index = len(nodes) + + nodes[index].update( + { + "address": index, + "word": self.pred[0], + "tag": self.pred[1], + "head": head, + "rel": rel, + } + ) + + for feature in sorted(self): + for item in sorted(self[feature]): + if isinstance(item, FStructure): + item._to_depgraph(nodes, index, feature) + elif isinstance(item, tuple): + new_index = len(nodes) + nodes[new_index].update( + { + "address": new_index, + "word": item[0], + "tag": item[1], + "head": index, + "rel": feature, + } + ) + elif isinstance(item, list): + for n in item: + n._to_depgraph(nodes, index, feature) + else: + raise Exception( + "feature %s is not an FStruct, a list, or a tuple" % feature + ) + + @staticmethod + def read_depgraph(depgraph): + return FStructure._read_depgraph(depgraph.root, depgraph) + + @staticmethod + def _read_depgraph(node, depgraph, label_counter=None, parent=None): + if not label_counter: + label_counter = Counter() + + if node["rel"].lower() in ["spec", "punct"]: + # the value of a 'spec' entry is a word, not an FStructure + return (node["word"], node["tag"]) + + else: + fstruct = FStructure() + fstruct.pred = None + fstruct.label = FStructure._make_label(label_counter.get()) + + fstruct.parent = parent + + word, tag = node["word"], node["tag"] + if tag[:2] == "VB": + if tag[2:3] == "D": + fstruct.safeappend("tense", ("PAST", "tense")) + fstruct.pred = (word, tag[:2]) + + if not fstruct.pred: + fstruct.pred = (word, tag) + + children = [ + depgraph.nodes[idx] + for idx in chain.from_iterable(node["deps"].values()) + ] + for child in children: + fstruct.safeappend( + child["rel"], + FStructure._read_depgraph(child, depgraph, label_counter, fstruct), + ) + + return fstruct + + @staticmethod + def _make_label(value): + """ + Pick an alphabetic character as identifier for an entity in the model. + + :param value: where to index into the list of characters + :type value: int + """ + letter = [ + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "a", + "b", + "c", + "d", + "e", + ][value - 1] + num = int(value) // 26 + if num > 0: + return letter + str(num) + else: + return letter + + def __repr__(self): + return self.__str__().replace("\n", "") + + def __str__(self): + return self.pretty_format() + + def pretty_format(self, indent=3): + try: + accum = "%s:[" % self.label + except NameError: + accum = "[" + try: + accum += "pred '%s'" % (self.pred[0]) + except NameError: + pass + + for feature in sorted(self): + for item in self[feature]: + if isinstance(item, FStructure): + next_indent = indent + len(feature) + 3 + len(self.label) + accum += "\n{}{} {}".format( + " " * (indent), + feature, + item.pretty_format(next_indent), + ) + elif isinstance(item, tuple): + accum += "\n{}{} '{}'".format(" " * (indent), feature, item[0]) + elif isinstance(item, list): + accum += "\n{}{} {{{}}}".format( + " " * (indent), + feature, + ("\n%s" % (" " * (indent + len(feature) + 2))).join(item), + ) + else: # ERROR + raise Exception( + "feature %s is not an FStruct, a list, or a tuple" % feature + ) + return accum + "]" + + +def demo_read_depgraph(): + from nltk.parse.dependencygraph import DependencyGraph + + dg1 = DependencyGraph( + """\ +Esso NNP 2 SUB +said VBD 0 ROOT +the DT 5 NMOD +Whiting NNP 5 NMOD +field NN 6 SUB +started VBD 2 VMOD +production NN 6 OBJ +Tuesday NNP 6 VMOD +""" + ) + dg2 = DependencyGraph( + """\ +John NNP 2 SUB +sees VBP 0 ROOT +Mary NNP 2 OBJ +""" + ) + dg3 = DependencyGraph( + """\ +a DT 2 SPEC +man NN 3 SUBJ +walks VB 0 ROOT +""" + ) + dg4 = DependencyGraph( + """\ +every DT 2 SPEC +girl NN 3 SUBJ +chases VB 0 ROOT +a DT 5 SPEC +dog NN 3 OBJ +""" + ) + + depgraphs = [dg1, dg2, dg3, dg4] + for dg in depgraphs: + print(FStructure.read_depgraph(dg)) + + +if __name__ == "__main__": + demo_read_depgraph() diff --git a/nltk/sem/linearlogic.py b/nltk/sem/linearlogic.py new file mode 100644 index 0000000..04ddd38 --- /dev/null +++ b/nltk/sem/linearlogic.py @@ -0,0 +1,481 @@ +# Natural Language Toolkit: Linear Logic +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +from nltk.internals import Counter +from nltk.sem.logic import APP, LogicParser + +_counter = Counter() + + +class Tokens: + # Punctuation + OPEN = "(" + CLOSE = ")" + + # Operations + IMP = "-o" + + PUNCT = [OPEN, CLOSE] + TOKENS = PUNCT + [IMP] + + +class LinearLogicParser(LogicParser): + """A linear logic expression parser.""" + + def __init__(self): + LogicParser.__init__(self) + + self.operator_precedence = {APP: 1, Tokens.IMP: 2, None: 3} + self.right_associated_operations += [Tokens.IMP] + + def get_all_symbols(self): + return Tokens.TOKENS + + def handle(self, tok, context): + if tok not in Tokens.TOKENS: + return self.handle_variable(tok, context) + elif tok == Tokens.OPEN: + return self.handle_open(tok, context) + + def get_BooleanExpression_factory(self, tok): + if tok == Tokens.IMP: + return ImpExpression + else: + return None + + def make_BooleanExpression(self, factory, first, second): + return factory(first, second) + + def attempt_ApplicationExpression(self, expression, context): + """Attempt to make an application expression. If the next tokens + are an argument in parens, then the argument expression is a + function being applied to the arguments. Otherwise, return the + argument expression.""" + if self.has_priority(APP, context): + if self.inRange(0) and self.token(0) == Tokens.OPEN: + self.token() # swallow then open paren + argument = self.process_next_expression(APP) + self.assertNextToken(Tokens.CLOSE) + expression = ApplicationExpression(expression, argument, None) + return expression + + def make_VariableExpression(self, name): + if name[0].isupper(): + return VariableExpression(name) + else: + return ConstantExpression(name) + + +class Expression: + _linear_logic_parser = LinearLogicParser() + + @classmethod + def fromstring(cls, s): + return cls._linear_logic_parser.parse(s) + + def applyto(self, other, other_indices=None): + return ApplicationExpression(self, other, other_indices) + + def __call__(self, other): + return self.applyto(other) + + def __repr__(self): + return f"<{self.__class__.__name__} {self}>" + + +class AtomicExpression(Expression): + def __init__(self, name, dependencies=None): + """ + :param name: str for the constant name + :param dependencies: list of int for the indices on which this atom is dependent + """ + assert isinstance(name, str) + self.name = name + + if not dependencies: + dependencies = [] + self.dependencies = dependencies + + def simplify(self, bindings=None): + """ + If 'self' is bound by 'bindings', return the atomic to which it is bound. + Otherwise, return self. + + :param bindings: ``BindingDict`` A dictionary of bindings used to simplify + :return: ``AtomicExpression`` + """ + if bindings and self in bindings: + return bindings[self] + else: + return self + + def compile_pos(self, index_counter, glueFormulaFactory): + """ + From Iddo Lev's PhD Dissertation p108-109 + + :param index_counter: ``Counter`` for unique indices + :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas + :return: (``Expression``,set) for the compiled linear logic and any newly created glue formulas + """ + self.dependencies = [] + return (self, []) + + def compile_neg(self, index_counter, glueFormulaFactory): + """ + From Iddo Lev's PhD Dissertation p108-109 + + :param index_counter: ``Counter`` for unique indices + :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas + :return: (``Expression``,set) for the compiled linear logic and any newly created glue formulas + """ + self.dependencies = [] + return (self, []) + + def initialize_labels(self, fstruct): + self.name = fstruct.initialize_label(self.name.lower()) + + def __eq__(self, other): + return self.__class__ == other.__class__ and self.name == other.name + + def __ne__(self, other): + return not self == other + + def __str__(self): + accum = self.name + if self.dependencies: + accum += "%s" % self.dependencies + return accum + + def __hash__(self): + return hash(self.name) + + +class ConstantExpression(AtomicExpression): + def unify(self, other, bindings): + """ + If 'other' is a constant, then it must be equal to 'self'. If 'other' is a variable, + then it must not be bound to anything other than 'self'. + + :param other: ``Expression`` + :param bindings: ``BindingDict`` A dictionary of all current bindings + :return: ``BindingDict`` A new combined dictionary of of 'bindings' and any new binding + :raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings' + """ + assert isinstance(other, Expression) + if isinstance(other, VariableExpression): + try: + return bindings + BindingDict([(other, self)]) + except VariableBindingException: + pass + elif self == other: + return bindings + raise UnificationException(self, other, bindings) + + +class VariableExpression(AtomicExpression): + def unify(self, other, bindings): + """ + 'self' must not be bound to anything other than 'other'. + + :param other: ``Expression`` + :param bindings: ``BindingDict`` A dictionary of all current bindings + :return: ``BindingDict`` A new combined dictionary of of 'bindings' and the new binding + :raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings' + """ + assert isinstance(other, Expression) + try: + if self == other: + return bindings + else: + return bindings + BindingDict([(self, other)]) + except VariableBindingException as e: + raise UnificationException(self, other, bindings) from e + + +class ImpExpression(Expression): + def __init__(self, antecedent, consequent): + """ + :param antecedent: ``Expression`` for the antecedent + :param consequent: ``Expression`` for the consequent + """ + assert isinstance(antecedent, Expression) + assert isinstance(consequent, Expression) + self.antecedent = antecedent + self.consequent = consequent + + def simplify(self, bindings=None): + return self.__class__( + self.antecedent.simplify(bindings), self.consequent.simplify(bindings) + ) + + def unify(self, other, bindings): + """ + Both the antecedent and consequent of 'self' and 'other' must unify. + + :param other: ``ImpExpression`` + :param bindings: ``BindingDict`` A dictionary of all current bindings + :return: ``BindingDict`` A new combined dictionary of of 'bindings' and any new bindings + :raise UnificationException: If 'self' and 'other' cannot be unified in the context of 'bindings' + """ + assert isinstance(other, ImpExpression) + try: + return ( + bindings + + self.antecedent.unify(other.antecedent, bindings) + + self.consequent.unify(other.consequent, bindings) + ) + except VariableBindingException as e: + raise UnificationException(self, other, bindings) from e + + def compile_pos(self, index_counter, glueFormulaFactory): + """ + From Iddo Lev's PhD Dissertation p108-109 + + :param index_counter: ``Counter`` for unique indices + :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas + :return: (``Expression``,set) for the compiled linear logic and any newly created glue formulas + """ + (a, a_new) = self.antecedent.compile_neg(index_counter, glueFormulaFactory) + (c, c_new) = self.consequent.compile_pos(index_counter, glueFormulaFactory) + return (ImpExpression(a, c), a_new + c_new) + + def compile_neg(self, index_counter, glueFormulaFactory): + """ + From Iddo Lev's PhD Dissertation p108-109 + + :param index_counter: ``Counter`` for unique indices + :param glueFormulaFactory: ``GlueFormula`` for creating new glue formulas + :return: (``Expression``,list of ``GlueFormula``) for the compiled linear logic and any newly created glue formulas + """ + (a, a_new) = self.antecedent.compile_pos(index_counter, glueFormulaFactory) + (c, c_new) = self.consequent.compile_neg(index_counter, glueFormulaFactory) + fresh_index = index_counter.get() + c.dependencies.append(fresh_index) + new_v = glueFormulaFactory("v%s" % fresh_index, a, {fresh_index}) + return (c, a_new + c_new + [new_v]) + + def initialize_labels(self, fstruct): + self.antecedent.initialize_labels(fstruct) + self.consequent.initialize_labels(fstruct) + + def __eq__(self, other): + return ( + self.__class__ == other.__class__ + and self.antecedent == other.antecedent + and self.consequent == other.consequent + ) + + def __ne__(self, other): + return not self == other + + def __str__(self): + return "{}{} {} {}{}".format( + Tokens.OPEN, + self.antecedent, + Tokens.IMP, + self.consequent, + Tokens.CLOSE, + ) + + def __hash__(self): + return hash(f"{hash(self.antecedent)}{Tokens.IMP}{hash(self.consequent)}") + + +class ApplicationExpression(Expression): + def __init__(self, function, argument, argument_indices=None): + """ + :param function: ``Expression`` for the function + :param argument: ``Expression`` for the argument + :param argument_indices: set for the indices of the glue formula from which the argument came + :raise LinearLogicApplicationException: If 'function' cannot be applied to 'argument' given 'argument_indices'. + """ + function_simp = function.simplify() + argument_simp = argument.simplify() + + assert isinstance(function_simp, ImpExpression) + assert isinstance(argument_simp, Expression) + + bindings = BindingDict() + + try: + if isinstance(function, ApplicationExpression): + bindings += function.bindings + if isinstance(argument, ApplicationExpression): + bindings += argument.bindings + bindings += function_simp.antecedent.unify(argument_simp, bindings) + except UnificationException as e: + raise LinearLogicApplicationException( + f"Cannot apply {function_simp} to {argument_simp}. {e}" + ) from e + + # If you are running it on complied premises, more conditions apply + if argument_indices: + # A.dependencies of (A -o (B -o C)) must be a proper subset of argument_indices + if not set(function_simp.antecedent.dependencies) < argument_indices: + raise LinearLogicApplicationException( + "Dependencies unfulfilled when attempting to apply Linear Logic formula %s to %s" + % (function_simp, argument_simp) + ) + if set(function_simp.antecedent.dependencies) == argument_indices: + raise LinearLogicApplicationException( + "Dependencies not a proper subset of indices when attempting to apply Linear Logic formula %s to %s" + % (function_simp, argument_simp) + ) + + self.function = function + self.argument = argument + self.bindings = bindings + + def simplify(self, bindings=None): + """ + Since function is an implication, return its consequent. There should be + no need to check that the application is valid since the checking is done + by the constructor. + + :param bindings: ``BindingDict`` A dictionary of bindings used to simplify + :return: ``Expression`` + """ + if not bindings: + bindings = self.bindings + + return self.function.simplify(bindings).consequent + + def __eq__(self, other): + return ( + self.__class__ == other.__class__ + and self.function == other.function + and self.argument == other.argument + ) + + def __ne__(self, other): + return not self == other + + def __str__(self): + return "%s" % self.function + Tokens.OPEN + "%s" % self.argument + Tokens.CLOSE + + def __hash__(self): + return hash(f"{hash(self.antecedent)}{Tokens.OPEN}{hash(self.consequent)}") + + +class BindingDict: + def __init__(self, bindings=None): + """ + :param bindings: + list [(``VariableExpression``, ``AtomicExpression``)] to initialize the dictionary + dict {``VariableExpression``: ``AtomicExpression``} to initialize the dictionary + """ + self.d = {} + + if isinstance(bindings, dict): + bindings = bindings.items() + + if bindings: + for v, b in bindings: + self[v] = b + + def __setitem__(self, variable, binding): + """ + A binding is consistent with the dict if its variable is not already bound, OR if its + variable is already bound to its argument. + + :param variable: ``VariableExpression`` The variable bind + :param binding: ``Expression`` The expression to which 'variable' should be bound + :raise VariableBindingException: If the variable cannot be bound in this dictionary + """ + assert isinstance(variable, VariableExpression) + assert isinstance(binding, Expression) + + assert variable != binding + + existing = self.d.get(variable, None) + + if not existing or binding == existing: + self.d[variable] = binding + else: + raise VariableBindingException( + "Variable %s already bound to another value" % (variable) + ) + + def __getitem__(self, variable): + """ + Return the expression to which 'variable' is bound + """ + assert isinstance(variable, VariableExpression) + + intermediate = self.d[variable] + while intermediate: + try: + intermediate = self.d[intermediate] + except KeyError: + return intermediate + + def __contains__(self, item): + return item in self.d + + def __add__(self, other): + """ + :param other: ``BindingDict`` The dict with which to combine self + :return: ``BindingDict`` A new dict containing all the elements of both parameters + :raise VariableBindingException: If the parameter dictionaries are not consistent with each other + """ + try: + combined = BindingDict() + for v in self.d: + combined[v] = self.d[v] + for v in other.d: + combined[v] = other.d[v] + return combined + except VariableBindingException as e: + raise VariableBindingException( + "Attempting to add two contradicting" + " VariableBindingsLists: %s, %s" % (self, other) + ) from e + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + if not isinstance(other, BindingDict): + raise TypeError + return self.d == other.d + + def __str__(self): + return "{" + ", ".join(f"{v}: {self.d[v]}" for v in sorted(self.d.keys())) + "}" + + def __repr__(self): + return "BindingDict: %s" % self + + +class VariableBindingException(Exception): + pass + + +class UnificationException(Exception): + def __init__(self, a, b, bindings): + Exception.__init__(self, f"Cannot unify {a} with {b} given {bindings}") + + +class LinearLogicApplicationException(Exception): + pass + + +def demo(): + lexpr = Expression.fromstring + + print(lexpr(r"f")) + print(lexpr(r"(g -o f)")) + print(lexpr(r"((g -o G) -o G)")) + print(lexpr(r"g -o h -o f")) + print(lexpr(r"(g -o f)(g)").simplify()) + print(lexpr(r"(H -o f)(g)").simplify()) + print(lexpr(r"((g -o G) -o G)((g -o f))").simplify()) + print(lexpr(r"(H -o H)((g -o f))").simplify()) + + +if __name__ == "__main__": + demo() diff --git a/nltk/sem/logic.py b/nltk/sem/logic.py new file mode 100644 index 0000000..fcd116e --- /dev/null +++ b/nltk/sem/logic.py @@ -0,0 +1,2109 @@ +# Natural Language Toolkit: Logic +# +# Author: Dan Garrette +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +A version of first order predicate logic, built on +top of the typed lambda calculus. +""" + +import operator +import re +from collections import defaultdict +from functools import reduce, total_ordering + +from nltk.internals import Counter +from nltk.util import Trie + +APP = "APP" + +_counter = Counter() + + +class Tokens: + LAMBDA = "\\" + LAMBDA_LIST = ["\\"] + + # Quantifiers + EXISTS = "exists" + EXISTS_LIST = ["some", "exists", "exist"] + ALL = "all" + ALL_LIST = ["all", "forall"] + IOTA = "iota" + IOTA_LIST = ["iota"] + + # Punctuation + DOT = "." + OPEN = "(" + CLOSE = ")" + COMMA = "," + + # Operations + NOT = "-" + NOT_LIST = ["not", "-", "!"] + AND = "&" + AND_LIST = ["and", "&", "^"] + OR = "|" + OR_LIST = ["or", "|"] + IMP = "->" + IMP_LIST = ["implies", "->", "=>"] + IFF = "<->" + IFF_LIST = ["iff", "<->", "<=>"] + EQ = "=" + EQ_LIST = ["=", "=="] + NEQ = "!=" + NEQ_LIST = ["!="] + + # Collections of tokens + BINOPS = AND_LIST + OR_LIST + IMP_LIST + IFF_LIST + QUANTS = EXISTS_LIST + ALL_LIST + IOTA_LIST + PUNCT = [DOT, OPEN, CLOSE, COMMA] + + TOKENS = BINOPS + EQ_LIST + NEQ_LIST + QUANTS + LAMBDA_LIST + PUNCT + NOT_LIST + + # Special + SYMBOLS = [x for x in TOKENS if re.match(r"^[-\\.(),!&^|>=<]*$", x)] + + +def boolean_ops(): + """ + Boolean operators + """ + names = ["negation", "conjunction", "disjunction", "implication", "equivalence"] + for pair in zip(names, [Tokens.NOT, Tokens.AND, Tokens.OR, Tokens.IMP, Tokens.IFF]): + print("%-15s\t%s" % pair) + + +def equality_preds(): + """ + Equality predicates + """ + names = ["equality", "inequality"] + for pair in zip(names, [Tokens.EQ, Tokens.NEQ]): + print("%-15s\t%s" % pair) + + +def binding_ops(): + """ + Binding operators + """ + names = ["existential", "universal", "lambda"] + for pair in zip(names, [Tokens.EXISTS, Tokens.ALL, Tokens.LAMBDA, Tokens.IOTA]): + print("%-15s\t%s" % pair) + + +class LogicParser: + """A lambda calculus expression parser.""" + + #: Maximum expression-nesting depth the recursive-descent parser will + #: descend to. Deeply nested input would otherwise recurse until Python + #: raises an uncaught ``RecursionError`` and crashes the caller + #: (uncontrolled recursion, CWE-674); past this depth a normal + #: ``LogicalExpressionException`` is raised instead. Configurable. + MAX_PARSE_DEPTH = 200 + + def __init__(self, type_check=False): + """ + :param type_check: should type checking be performed + to their types? + :type type_check: bool + """ + assert isinstance(type_check, bool) + + self._currentIndex = 0 + self._buffer = [] + self._parse_depth = 0 + self.type_check = type_check + + """A list of tuples of quote characters. The 4-tuple is comprised + of the start character, the end character, the escape character, and + a boolean indicating whether the quotes should be included in the + result. Quotes are used to signify that a token should be treated as + atomic, ignoring any special characters within the token. The escape + character allows the quote end character to be used within the quote. + If True, the boolean indicates that the final token should contain the + quote and escape characters. + This method exists to be overridden""" + self.quote_chars = [] + + self.operator_precedence = dict( + [(x, 1) for x in Tokens.LAMBDA_LIST] + + [(x, 2) for x in Tokens.NOT_LIST] + + [(APP, 3)] + + [(x, 4) for x in Tokens.EQ_LIST + Tokens.NEQ_LIST] + + [(x, 5) for x in Tokens.QUANTS] + + [(x, 6) for x in Tokens.AND_LIST] + + [(x, 7) for x in Tokens.OR_LIST] + + [(x, 8) for x in Tokens.IMP_LIST] + + [(x, 9) for x in Tokens.IFF_LIST] + + [(None, 10)] + ) + self.right_associated_operations = [APP] + + def parse(self, data, signature=None): + """ + Parse the expression. + + :param data: str for the input to be parsed + :param signature: ``dict`` that maps variable names to type + strings + :returns: a parsed Expression + """ + data = data.rstrip() + + self._currentIndex = 0 + self._parse_depth = 0 + self._buffer, mapping = self.process(data) + + try: + result = self.process_next_expression(None) + if self.inRange(0): + raise UnexpectedTokenException(self._currentIndex + 1, self.token(0)) + except LogicalExpressionException as e: + msg = "{}\n{}\n{}^".format(e, data, " " * mapping[e.index - 1]) + raise LogicalExpressionException(None, msg) from e + + if self.type_check: + result.typecheck(signature) + + return result + + def process(self, data): + """Split the data into tokens""" + out = [] + mapping = {} + tokenTrie = Trie(self.get_all_symbols()) + token = "" + data_idx = 0 + token_start_idx = data_idx + while data_idx < len(data): + cur_data_idx = data_idx + quoted_token, data_idx = self.process_quoted_token(data_idx, data) + if quoted_token: + if not token: + token_start_idx = cur_data_idx + token += quoted_token + continue + + st = tokenTrie + c = data[data_idx] + symbol = "" + while c in st: + symbol += c + st = st[c] + if len(data) - data_idx > len(symbol): + c = data[data_idx + len(symbol)] + else: + break + if Trie.LEAF in st: + # token is a complete symbol + if token: + mapping[len(out)] = token_start_idx + out.append(token) + token = "" + mapping[len(out)] = data_idx + out.append(symbol) + data_idx += len(symbol) + else: + if data[data_idx] in " \t\n": # any whitespace + if token: + mapping[len(out)] = token_start_idx + out.append(token) + token = "" + else: + if not token: + token_start_idx = data_idx + token += data[data_idx] + data_idx += 1 + if token: + mapping[len(out)] = token_start_idx + out.append(token) + mapping[len(out)] = len(data) + mapping[len(out) + 1] = len(data) + 1 + return out, mapping + + def process_quoted_token(self, data_idx, data): + token = "" + c = data[data_idx] + i = data_idx + for start, end, escape, incl_quotes in self.quote_chars: + if c == start: + if incl_quotes: + token += c + i += 1 + while data[i] != end: + if data[i] == escape: + if incl_quotes: + token += data[i] + i += 1 + if len(data) == i: # if there are no more chars + raise LogicalExpressionException( + None, + "End of input reached. " + "Escape character [%s] found at end." % escape, + ) + token += data[i] + else: + token += data[i] + i += 1 + if len(data) == i: + raise LogicalExpressionException( + None, "End of input reached. " "Expected: [%s]" % end + ) + if incl_quotes: + token += data[i] + i += 1 + if not token: + raise LogicalExpressionException(None, "Empty quoted token found") + break + return token, i + + def get_all_symbols(self): + """This method exists to be overridden""" + return Tokens.SYMBOLS + + def inRange(self, location): + """Return TRUE if the given location is within the buffer""" + return self._currentIndex + location < len(self._buffer) + + def token(self, location=None): + """Get the next waiting token. If a location is given, then + return the token at currentIndex+location without advancing + currentIndex; setting it gives lookahead/lookback capability.""" + try: + if location is None: + tok = self._buffer[self._currentIndex] + self._currentIndex += 1 + else: + tok = self._buffer[self._currentIndex + location] + return tok + except IndexError as e: + raise ExpectedMoreTokensException(self._currentIndex + 1) from e + + def isvariable(self, tok): + return tok not in Tokens.TOKENS + + def process_next_expression(self, context): + """Parse the next complete expression from the stream and return it.""" + self._parse_depth += 1 + try: + if self._parse_depth > self.MAX_PARSE_DEPTH: + # Use 1-based index (consistent with ExpectedMoreTokensException); + # parse() formats the caret via mapping[index - 1], so index 0 + # (limit hit before any token is consumed) must be avoided. + raise LogicalExpressionException( + self._currentIndex + 1, + "Expression nesting exceeds the maximum depth (%d)." + % self.MAX_PARSE_DEPTH, + ) + + try: + tok = self.token() + except ExpectedMoreTokensException as e: + raise ExpectedMoreTokensException( + self._currentIndex + 1, message="Expression expected." + ) from e + + accum = self.handle(tok, context) + + if not accum: + raise UnexpectedTokenException( + self._currentIndex, tok, message="Expression expected." + ) + + return self.attempt_adjuncts(accum, context) + finally: + self._parse_depth -= 1 + + def handle(self, tok, context): + """This method is intended to be overridden for logics that + use different operators or expressions""" + if self.isvariable(tok): + return self.handle_variable(tok, context) + + elif tok in Tokens.NOT_LIST: + return self.handle_negation(tok, context) + + elif tok in Tokens.LAMBDA_LIST: + return self.handle_lambda(tok, context) + + elif tok in Tokens.QUANTS: + return self.handle_quant(tok, context) + + elif tok == Tokens.OPEN: + return self.handle_open(tok, context) + + def attempt_adjuncts(self, expression, context): + cur_idx = None + while cur_idx != self._currentIndex: # while adjuncts are added + cur_idx = self._currentIndex + expression = self.attempt_EqualityExpression(expression, context) + expression = self.attempt_ApplicationExpression(expression, context) + expression = self.attempt_BooleanExpression(expression, context) + return expression + + def handle_negation(self, tok, context): + return self.make_NegatedExpression(self.process_next_expression(Tokens.NOT)) + + def make_NegatedExpression(self, expression): + return NegatedExpression(expression) + + def handle_variable(self, tok, context): + # It's either: 1) a predicate expression: sees(x,y) + # 2) an application expression: P(x) + # 3) a solo variable: john OR x + accum = self.make_VariableExpression(tok) + if self.inRange(0) and self.token(0) == Tokens.OPEN: + # The predicate has arguments + if not isinstance(accum, FunctionVariableExpression) and not isinstance( + accum, ConstantExpression + ): + raise LogicalExpressionException( + self._currentIndex, + "'%s' is an illegal predicate name. " + "Individual variables may not be used as " + "predicates." % tok, + ) + self.token() # swallow the Open Paren + + # curry the arguments + accum = self.make_ApplicationExpression( + accum, self.process_next_expression(APP) + ) + while self.inRange(0) and self.token(0) == Tokens.COMMA: + self.token() # swallow the comma + accum = self.make_ApplicationExpression( + accum, self.process_next_expression(APP) + ) + self.assertNextToken(Tokens.CLOSE) + return accum + + def get_next_token_variable(self, description): + try: + tok = self.token() + except ExpectedMoreTokensException as e: + raise ExpectedMoreTokensException(e.index, "Variable expected.") from e + if isinstance(self.make_VariableExpression(tok), ConstantExpression): + raise LogicalExpressionException( + self._currentIndex, + "'%s' is an illegal variable name. " + "Constants may not be %s." % (tok, description), + ) + return Variable(tok) + + def handle_lambda(self, tok, context): + # Expression is a lambda expression + if not self.inRange(0): + raise ExpectedMoreTokensException( + self._currentIndex + 2, + message="Variable and Expression expected following lambda operator.", + ) + vars = [self.get_next_token_variable("abstracted")] + while True: + if not self.inRange(0) or ( + self.token(0) == Tokens.DOT and not self.inRange(1) + ): + raise ExpectedMoreTokensException( + self._currentIndex + 2, message="Expression expected." + ) + if not self.isvariable(self.token(0)): + break + # Support expressions like: \x y.M == \x.\y.M + vars.append(self.get_next_token_variable("abstracted")) + if self.inRange(0) and self.token(0) == Tokens.DOT: + self.token() # swallow the dot + + accum = self.process_next_expression(tok) + while vars: + accum = self.make_LambdaExpression(vars.pop(), accum) + return accum + + def handle_quant(self, tok, context): + # Expression is a quantified expression: some x.M + factory = self.get_QuantifiedExpression_factory(tok) + + if not self.inRange(0): + raise ExpectedMoreTokensException( + self._currentIndex + 2, + message="Variable and Expression expected following quantifier '%s'." + % tok, + ) + vars = [self.get_next_token_variable("quantified")] + while True: + if not self.inRange(0) or ( + self.token(0) == Tokens.DOT and not self.inRange(1) + ): + raise ExpectedMoreTokensException( + self._currentIndex + 2, message="Expression expected." + ) + if not self.isvariable(self.token(0)): + break + # Support expressions like: some x y.M == some x.some y.M + vars.append(self.get_next_token_variable("quantified")) + if self.inRange(0) and self.token(0) == Tokens.DOT: + self.token() # swallow the dot + + accum = self.process_next_expression(tok) + while vars: + accum = self.make_QuanifiedExpression(factory, vars.pop(), accum) + return accum + + def get_QuantifiedExpression_factory(self, tok): + """This method serves as a hook for other logic parsers that + have different quantifiers""" + if tok in Tokens.EXISTS_LIST: + return ExistsExpression + elif tok in Tokens.ALL_LIST: + return AllExpression + elif tok in Tokens.IOTA_LIST: + return IotaExpression + else: + self.assertToken(tok, Tokens.QUANTS) + + def make_QuanifiedExpression(self, factory, variable, term): + return factory(variable, term) + + def handle_open(self, tok, context): + # Expression is in parens + accum = self.process_next_expression(None) + self.assertNextToken(Tokens.CLOSE) + return accum + + def attempt_EqualityExpression(self, expression, context): + """Attempt to make an equality expression. If the next token is an + equality operator, then an EqualityExpression will be returned. + Otherwise, the parameter will be returned.""" + if self.inRange(0): + tok = self.token(0) + if tok in Tokens.EQ_LIST + Tokens.NEQ_LIST and self.has_priority( + tok, context + ): + self.token() # swallow the "=" or "!=" + expression = self.make_EqualityExpression( + expression, self.process_next_expression(tok) + ) + if tok in Tokens.NEQ_LIST: + expression = self.make_NegatedExpression(expression) + return expression + + def make_EqualityExpression(self, first, second): + """This method serves as a hook for other logic parsers that + have different equality expression classes""" + return EqualityExpression(first, second) + + def attempt_BooleanExpression(self, expression, context): + """Attempt to make a boolean expression. If the next token is a boolean + operator, then a BooleanExpression will be returned. Otherwise, the + parameter will be returned.""" + while self.inRange(0): + tok = self.token(0) + factory = self.get_BooleanExpression_factory(tok) + if factory and self.has_priority(tok, context): + self.token() # swallow the operator + expression = self.make_BooleanExpression( + factory, expression, self.process_next_expression(tok) + ) + else: + break + return expression + + def get_BooleanExpression_factory(self, tok): + """This method serves as a hook for other logic parsers that + have different boolean operators""" + if tok in Tokens.AND_LIST: + return AndExpression + elif tok in Tokens.OR_LIST: + return OrExpression + elif tok in Tokens.IMP_LIST: + return ImpExpression + elif tok in Tokens.IFF_LIST: + return IffExpression + else: + return None + + def make_BooleanExpression(self, factory, first, second): + return factory(first, second) + + def attempt_ApplicationExpression(self, expression, context): + """Attempt to make an application expression. The next tokens are + a list of arguments in parens, then the argument expression is a + function being applied to the arguments. Otherwise, return the + argument expression.""" + if self.has_priority(APP, context): + if self.inRange(0) and self.token(0) == Tokens.OPEN: + if ( + not isinstance(expression, LambdaExpression) + and not isinstance(expression, ApplicationExpression) + and not isinstance(expression, FunctionVariableExpression) + and not isinstance(expression, ConstantExpression) + ): + raise LogicalExpressionException( + self._currentIndex, + ("The function '%s" % expression) + + "' is not a Lambda Expression, an " + "Application Expression, or a " + "functional predicate, so it may " + "not take arguments.", + ) + self.token() # swallow then open paren + # curry the arguments + accum = self.make_ApplicationExpression( + expression, self.process_next_expression(APP) + ) + while self.inRange(0) and self.token(0) == Tokens.COMMA: + self.token() # swallow the comma + accum = self.make_ApplicationExpression( + accum, self.process_next_expression(APP) + ) + self.assertNextToken(Tokens.CLOSE) + return accum + return expression + + def make_ApplicationExpression(self, function, argument): + return ApplicationExpression(function, argument) + + def make_VariableExpression(self, name): + return VariableExpression(Variable(name)) + + def make_LambdaExpression(self, variable, term): + return LambdaExpression(variable, term) + + def has_priority(self, operation, context): + return self.operator_precedence[operation] < self.operator_precedence[ + context + ] or ( + operation in self.right_associated_operations + and self.operator_precedence[operation] == self.operator_precedence[context] + ) + + def assertNextToken(self, expected): + try: + tok = self.token() + except ExpectedMoreTokensException as e: + raise ExpectedMoreTokensException( + e.index, message="Expected token '%s'." % expected + ) from e + + if isinstance(expected, list): + if tok not in expected: + raise UnexpectedTokenException(self._currentIndex, tok, expected) + else: + if tok != expected: + raise UnexpectedTokenException(self._currentIndex, tok, expected) + + def assertToken(self, tok, expected): + if isinstance(expected, list): + if tok not in expected: + raise UnexpectedTokenException(self._currentIndex, tok, expected) + else: + if tok != expected: + raise UnexpectedTokenException(self._currentIndex, tok, expected) + + def __repr__(self): + if self.inRange(0): + msg = "Next token: " + self.token(0) + else: + msg = "No more tokens" + return "<" + self.__class__.__name__ + ": " + msg + ">" + + +def read_logic(s, logic_parser=None, encoding=None): + """ + Convert a file of First Order Formulas into a list of {Expression}s. + + :param s: the contents of the file + :type s: str + :param logic_parser: The parser to be used to parse the logical expression + :type logic_parser: LogicParser + :param encoding: the encoding of the input string, if it is binary + :type encoding: str + :return: a list of parsed formulas. + :rtype: list(Expression) + """ + if encoding is not None: + s = s.decode(encoding) + if logic_parser is None: + logic_parser = LogicParser() + + statements = [] + for linenum, line in enumerate(s.splitlines()): + line = line.strip() + if line.startswith("#") or line == "": + continue + try: + statements.append(logic_parser.parse(line)) + except LogicalExpressionException as e: + raise ValueError(f"Unable to parse line {linenum}: {line}") from e + return statements + + +@total_ordering +class Variable: + def __init__(self, name): + """ + :param name: the name of the variable + """ + assert isinstance(name, str), "%s is not a string" % name + self.name = name + + def __eq__(self, other): + return isinstance(other, Variable) and self.name == other.name + + def __lt__(self, other): + if not isinstance(other, Variable): + raise TypeError + return self.name < other.name + + def substitute_bindings(self, bindings): + return bindings.get(self, self) + + def __hash__(self): + return hash(self.name) + + def __str__(self): + return self.name + + def __repr__(self): + return "Variable('%s')" % self.name + + +def unique_variable(pattern=None, ignore=None): + """ + Return a new, unique variable. + + :param pattern: ``Variable`` that is being replaced. The new variable must + be the same type. + :param term: a set of ``Variable`` objects that should not be returned from + this function. + :rtype: Variable + """ + if pattern is not None: + if is_indvar(pattern.name): + prefix = "z" + elif is_funcvar(pattern.name): + prefix = "F" + elif is_eventvar(pattern.name): + prefix = "e0" + else: + assert False, "Cannot generate a unique constant" + else: + prefix = "z" + + v = Variable(f"{prefix}{_counter.get()}") + while ignore is not None and v in ignore: + v = Variable(f"{prefix}{_counter.get()}") + return v + + +def skolem_function(univ_scope=None): + """ + Return a skolem function over the variables in univ_scope + param univ_scope + """ + skolem = VariableExpression(Variable("F%s" % _counter.get())) + if univ_scope: + for v in list(univ_scope): + skolem = skolem(VariableExpression(v)) + return skolem + + +class Type: + def __repr__(self): + return "%s" % self + + def __hash__(self): + return hash("%s" % self) + + @classmethod + def fromstring(cls, s): + return read_type(s) + + +class ComplexType(Type): + def __init__(self, first, second): + assert isinstance(first, Type), "%s is not a Type" % first + assert isinstance(second, Type), "%s is not a Type" % second + self.first = first + self.second = second + + def __eq__(self, other): + return ( + isinstance(other, ComplexType) + and self.first == other.first + and self.second == other.second + ) + + __hash__ = Type.__hash__ + + def matches(self, other): + if isinstance(other, ComplexType): + return self.first.matches(other.first) and self.second.matches(other.second) + else: + return self == ANY_TYPE + + def resolve(self, other): + if other == ANY_TYPE: + return self + elif isinstance(other, ComplexType): + f = self.first.resolve(other.first) + s = self.second.resolve(other.second) + if f and s: + return ComplexType(f, s) + else: + return None + elif self == ANY_TYPE: + return other + else: + return None + + def __str__(self): + if self == ANY_TYPE: + return "%s" % ANY_TYPE + else: + return f"<{self.first},{self.second}>" + + def str(self): + if self == ANY_TYPE: + return ANY_TYPE.str() + else: + return f"({self.first.str()} -> {self.second.str()})" + + +class BasicType(Type): + def __eq__(self, other): + return isinstance(other, BasicType) and ("%s" % self) == ("%s" % other) + + __hash__ = Type.__hash__ + + def matches(self, other): + return other == ANY_TYPE or self == other + + def resolve(self, other): + if self.matches(other): + return self + else: + return None + + +class EntityType(BasicType): + def __str__(self): + return "e" + + def str(self): + return "IND" + + +class TruthValueType(BasicType): + def __str__(self): + return "t" + + def str(self): + return "BOOL" + + +class EventType(BasicType): + def __str__(self): + return "v" + + def str(self): + return "EVENT" + + +class AnyType(BasicType, ComplexType): + def __init__(self): + pass + + @property + def first(self): + return self + + @property + def second(self): + return self + + def __eq__(self, other): + return isinstance(other, AnyType) or other.__eq__(self) + + __hash__ = Type.__hash__ + + def matches(self, other): + return True + + def resolve(self, other): + return other + + def __str__(self): + return "?" + + def str(self): + return "ANY" + + +TRUTH_TYPE = TruthValueType() +ENTITY_TYPE = EntityType() +EVENT_TYPE = EventType() +ANY_TYPE = AnyType() + + +def read_type(type_string): + assert isinstance(type_string, str) + type_string = type_string.replace(" ", "") # remove spaces + + if type_string[0] == "<": + assert type_string[-1] == ">" + paren_count = 0 + for i, char in enumerate(type_string): + if char == "<": + paren_count += 1 + elif char == ">": + paren_count -= 1 + assert paren_count > 0 + elif char == ",": + if paren_count == 1: + break + return ComplexType( + read_type(type_string[1:i]), read_type(type_string[i + 1 : -1]) + ) + elif type_string[0] == "%s" % ENTITY_TYPE: + return ENTITY_TYPE + elif type_string[0] == "%s" % TRUTH_TYPE: + return TRUTH_TYPE + elif type_string[0] == "%s" % ANY_TYPE: + return ANY_TYPE + else: + raise LogicalExpressionException( + None, "Unexpected character: '%s'." % type_string[0] + ) + + +class TypeException(Exception): + def __init__(self, msg): + super().__init__(msg) + + +class InconsistentTypeHierarchyException(TypeException): + def __init__(self, variable, expression=None): + if expression: + msg = ( + "The variable '%s' was found in multiple places with different" + " types in '%s'." % (variable, expression) + ) + else: + msg = ( + "The variable '%s' was found in multiple places with different" + " types." % (variable) + ) + super().__init__(msg) + + +class TypeResolutionException(TypeException): + def __init__(self, expression, other_type): + super().__init__( + "The type of '%s', '%s', cannot be resolved with type '%s'" + % (expression, expression.type, other_type) + ) + + +class IllegalTypeException(TypeException): + def __init__(self, expression, other_type, allowed_type): + super().__init__( + "Cannot set type of %s '%s' to '%s'; must match type '%s'." + % (expression.__class__.__name__, expression, other_type, allowed_type) + ) + + +def typecheck(expressions, signature=None): + """ + Ensure correct typing across a collection of ``Expression`` objects. + :param expressions: a collection of expressions + :param signature: dict that maps variable names to types (or string + representations of types) + """ + # typecheck and create master signature + for expression in expressions: + signature = expression.typecheck(signature) + # apply master signature to all expressions + for expression in expressions[:-1]: + expression.typecheck(signature) + return signature + + +class SubstituteBindingsI: + """ + An interface for classes that can perform substitutions for + variables. + """ + + def substitute_bindings(self, bindings): + """ + :return: The object that is obtained by replacing + each variable bound by ``bindings`` with its values. + Aliases are already resolved. (maybe?) + :rtype: (any) + """ + raise NotImplementedError() + + def variables(self): + """ + :return: A list of all variables in this object. + """ + raise NotImplementedError() + + +class Expression(SubstituteBindingsI): + """This is the base abstract object for all logical expressions""" + + _logic_parser = LogicParser() + _type_checking_logic_parser = LogicParser(type_check=True) + + @classmethod + def fromstring(cls, s, type_check=False, signature=None): + if type_check: + return cls._type_checking_logic_parser.parse(s, signature) + else: + return cls._logic_parser.parse(s, signature) + + def __call__(self, other, *additional): + accum = self.applyto(other) + for a in additional: + accum = accum(a) + return accum + + def applyto(self, other): + assert isinstance(other, Expression), "%s is not an Expression" % other + return ApplicationExpression(self, other) + + def __neg__(self): + return NegatedExpression(self) + + def negate(self): + """If this is a negated expression, remove the negation. + Otherwise add a negation.""" + return -self + + def __and__(self, other): + if not isinstance(other, Expression): + raise TypeError("%s is not an Expression" % other) + return AndExpression(self, other) + + def __or__(self, other): + if not isinstance(other, Expression): + raise TypeError("%s is not an Expression" % other) + return OrExpression(self, other) + + def __gt__(self, other): + if not isinstance(other, Expression): + raise TypeError("%s is not an Expression" % other) + return ImpExpression(self, other) + + def __lt__(self, other): + if not isinstance(other, Expression): + raise TypeError("%s is not an Expression" % other) + return IffExpression(self, other) + + def __eq__(self, other): + return NotImplemented + + def __hash__(self): + return hash(repr(self)) + + def equiv(self, other, prover=None): + """ + Check for logical equivalence. + Pass the expression (self <-> other) to the theorem prover. + If the prover says it is valid, then the self and other are equal. + + :param other: an ``Expression`` to check equality against + :param prover: a ``nltk.inference.api.Prover`` + """ + assert isinstance(other, Expression), "%s is not an Expression" % other + + if prover is None: + from nltk.inference import Prover9 + + prover = Prover9() + bicond = IffExpression(self.simplify(), other.simplify()) + return prover.prove(bicond) + + def substitute_bindings(self, bindings): + expr = self + for var in expr.variables(): + if var in bindings: + val = bindings[var] + if isinstance(val, Variable): + val = self.make_VariableExpression(val) + elif not isinstance(val, Expression): + raise ValueError( + "Can not substitute a non-expression " + "value into an expression: %r" % (val,) + ) + # Substitute bindings in the target value. + val = val.substitute_bindings(bindings) + # Replace var w/ the target value. + expr = expr.replace(var, val, alpha_convert=True) + return expr.simplify() + + def typecheck(self, signature=None): + """ + Infer and check types. Raise exceptions if necessary. + + :param signature: dict that maps variable names to types (or string + representations of types) + :return: the signature, plus any additional type mappings + """ + sig = defaultdict(list) + if signature: + for key in signature: + val = signature[key] + varEx = VariableExpression(Variable(key)) + if isinstance(val, Type): + varEx.type = val + else: + varEx.type = read_type(val) + sig[key].append(varEx) + + self._set_type(signature=sig) + + return {key: sig[key][0].type for key in sig} + + def findtype(self, variable): + """ + Find the type of the given variable as it is used in this expression. + For example, finding the type of "P" in "P(x) & Q(x,y)" yields "" + + :param variable: Variable + """ + raise NotImplementedError() + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """ + Set the type of this expression to be the given type. Raise type + exceptions where applicable. + + :param other_type: Type + :param signature: dict(str -> list(AbstractVariableExpression)) + """ + raise NotImplementedError() + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + """ + Replace every instance of 'variable' with 'expression' + :param variable: ``Variable`` The variable to replace + :param expression: ``Expression`` The expression with which to replace it + :param replace_bound: bool Should bound variables be replaced? + :param alpha_convert: bool Alpha convert automatically to avoid name clashes? + """ + assert isinstance(variable, Variable), "%s is not a Variable" % variable + assert isinstance(expression, Expression), ( + "%s is not an Expression" % expression + ) + + return self.visit_structured( + lambda e: e.replace(variable, expression, replace_bound, alpha_convert), + self.__class__, + ) + + def normalize(self, newvars=None): + """Rename auto-generated unique variables""" + + def get_indiv_vars(e): + if isinstance(e, IndividualVariableExpression): + return {e} + elif isinstance(e, AbstractVariableExpression): + return set() + else: + return e.visit( + get_indiv_vars, lambda parts: reduce(operator.or_, parts, set()) + ) + + result = self + for i, e in enumerate(sorted(get_indiv_vars(self), key=lambda e: e.variable)): + if isinstance(e, EventVariableExpression): + newVar = e.__class__(Variable("e0%s" % (i + 1))) + elif isinstance(e, IndividualVariableExpression): + newVar = e.__class__(Variable("z%s" % (i + 1))) + else: + newVar = e + result = result.replace(e.variable, newVar, True) + return result + + def visit(self, function, combinator): + """ + Recursively visit subexpressions. Apply 'function' to each + subexpression and pass the result of each function application + to the 'combinator' for aggregation: + + return combinator(map(function, self.subexpressions)) + + Bound variables are neither applied upon by the function nor given to + the combinator. + :param function: ``Function`` to call on each subexpression + :param combinator: ``Function,R>`` to combine the results of the + function calls + :return: result of combination ``R`` + """ + raise NotImplementedError() + + def visit_structured(self, function, combinator): + """ + Recursively visit subexpressions. Apply 'function' to each + subexpression and pass the result of each function application + to the 'combinator' for aggregation. The combinator must have + the same signature as the constructor. The function is not + applied to bound variables, but they are passed to the + combinator. + :param function: ``Function`` to call on each subexpression + :param combinator: ``Function`` with the same signature as the + constructor, to combine the results of the function calls + :return: result of combination + """ + return self.visit(function, lambda parts: combinator(*parts)) + + def __repr__(self): + return f"<{self.__class__.__name__} {self}>" + + def __str__(self): + return self.str() + + def variables(self): + """ + Return a set of all the variables for binding substitution. + The variables returned include all free (non-bound) individual + variables and any variable starting with '?' or '@'. + :return: set of ``Variable`` objects + """ + return self.free() | { + p for p in self.predicates() | self.constants() if re.match("^[?@]", p.name) + } + + def free(self): + """ + Return a set of all the free (non-bound) variables. This includes + both individual and predicate variables, but not constants. + :return: set of ``Variable`` objects + """ + return self.visit( + lambda e: e.free(), lambda parts: reduce(operator.or_, parts, set()) + ) + + def constants(self): + """ + Return a set of individual constants (non-predicates). + :return: set of ``Variable`` objects + """ + return self.visit( + lambda e: e.constants(), lambda parts: reduce(operator.or_, parts, set()) + ) + + def predicates(self): + """ + Return a set of predicates (constants, not variables). + :return: set of ``Variable`` objects + """ + return self.visit( + lambda e: e.predicates(), lambda parts: reduce(operator.or_, parts, set()) + ) + + def simplify(self): + """ + :return: beta-converted version of this expression + """ + return self.visit_structured(lambda e: e.simplify(), self.__class__) + + def make_VariableExpression(self, variable): + return VariableExpression(variable) + + +#: Upper bound on the number of subexpressions a single beta-reduction may +#: produce in :meth:`ApplicationExpression.simplify`. Beta reduction copies the +#: argument once per occurrence of the bound variable, so a tiny expression built +#: from nested duplicating lambdas (e.g. ``(\Y.(Y & Y))`` applied repeatedly) +#: reduces to an exponentially large normal form and exhausts CPU and memory +#: (CWE-400). ``simplify`` refuses with a ``ValueError`` once a reduction's result +#: exceeds this size. Realistic logical expressions have only hundreds to low +#: thousands of subexpressions, so this default leaves roughly an order of +#: magnitude of headroom while still bounding the blow-up; raise it for the rare +#: genuinely large expression. +MAX_SIMPLIFY_SIZE = 10_000 + + +def _exceeds_size(expression, limit): + """Return ``True`` if ``expression`` has more than ``limit`` subexpressions. + + Counts iteratively (so deep expressions can't overflow the stack) and stops + as soon as the limit is passed, so the check costs ``O(limit)`` even for an + exponentially large expression. + """ + count = 0 + stack = [expression] + while stack: + node = stack.pop() + count += 1 + if count > limit: + return True + # Leaves (AbstractVariableExpression) have no subexpressions and do not + # implement ``visit``; every other expression pushes its children. + if not isinstance(node, AbstractVariableExpression): + node.visit(stack.append, lambda parts: None) + return False + + +class ApplicationExpression(Expression): + r""" + This class is used to represent two related types of logical expressions. + + The first is a Predicate Expression, such as "P(x,y)". A predicate + expression is comprised of a ``FunctionVariableExpression`` or + ``ConstantExpression`` as the predicate and a list of Expressions as the + arguments. + + The second is a an application of one expression to another, such as + "(\x.dog(x))(fido)". + + The reason Predicate Expressions are treated as Application Expressions is + that the Variable Expression predicate of the expression may be replaced + with another Expression, such as a LambdaExpression, which would mean that + the Predicate should be thought of as being applied to the arguments. + + The logical expression reader will always curry arguments in a application expression. + So, "\x y.see(x,y)(john,mary)" will be represented internally as + "((\x y.(see(x))(y))(john))(mary)". This simplifies the internals since + there will always be exactly one argument in an application. + + The str() method will usually print the curried forms of application + expressions. The one exception is when the the application expression is + really a predicate expression (ie, underlying function is an + ``AbstractVariableExpression``). This means that the example from above + will be returned as "(\x y.see(x,y)(john))(mary)". + """ + + def __init__(self, function, argument): + """ + :param function: ``Expression``, for the function expression + :param argument: ``Expression``, for the argument + """ + assert isinstance(function, Expression), "%s is not an Expression" % function + assert isinstance(argument, Expression), "%s is not an Expression" % argument + self.function = function + self.argument = argument + + def simplify(self): + function = self.function.simplify() + argument = self.argument.simplify() + if isinstance(function, LambdaExpression): + # Rely strictly on NLTK's native capture-avoidance during substitution + # without burning global variable counters proactively. + result = function.term.replace( + function.variable, argument, alpha_convert=True + ) + # Beta reduction copies the argument once per occurrence of the bound + # variable, so nested duplicating lambdas (e.g. ``\Y.(Y & Y)``) blow + # up exponentially in time and memory (CWE-400). Refuse once a single + # reduction's result exceeds the size budget; nested reductions are + # each checked, so the first that overflows stops the whole search. + if _exceeds_size(result, MAX_SIMPLIFY_SIZE): + raise ValueError( + f"Refusing to beta-reduce: result exceeds MAX_SIMPLIFY_SIZE " + f"({MAX_SIMPLIFY_SIZE}) subexpressions; the expression may " + f"cause exponential blow-up (CWE-400). Raise " + f"nltk.sem.logic.MAX_SIMPLIFY_SIZE to allow it." + ) + return result.simplify() + else: + return self.__class__(function, argument) + + @property + def type(self): + if isinstance(self.function.type, ComplexType): + return self.function.type.second + else: + return ANY_TYPE + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + self.argument._set_type(ANY_TYPE, signature) + try: + self.function._set_type( + ComplexType(self.argument.type, other_type), signature + ) + except TypeResolutionException as e: + raise TypeException( + "The function '%s' is of type '%s' and cannot be applied " + "to '%s' of type '%s'. Its argument must match type '%s'." + % ( + self.function, + self.function.type, + self.argument, + self.argument.type, + self.function.type.first, + ) + ) from e + + def findtype(self, variable): + """:see Expression.findtype()""" + assert isinstance(variable, Variable), "%s is not a Variable" % variable + if self.is_atom(): + function, args = self.uncurry() + else: + # It's not a predicate expression ("P(x,y)"), so leave args curried + function = self.function + args = [self.argument] + + found = [arg.findtype(variable) for arg in [function] + args] + + unique = [] + for f in found: + if f != ANY_TYPE: + if unique: + for u in unique: + if f.matches(u): + break + else: + unique.append(f) + + if len(unique) == 1: + return list(unique)[0] + else: + return ANY_TYPE + + def constants(self): + """:see: Expression.constants()""" + if isinstance(self.function, AbstractVariableExpression): + function_constants = set() + else: + function_constants = self.function.constants() + return function_constants | self.argument.constants() + + def predicates(self): + """:see: Expression.predicates()""" + if isinstance(self.function, ConstantExpression): + function_preds = {self.function.variable} + else: + function_preds = self.function.predicates() + return function_preds | self.argument.predicates() + + def visit(self, function, combinator): + """:see: Expression.visit()""" + return combinator([function(self.function), function(self.argument)]) + + def __eq__(self, other): + return ( + isinstance(other, ApplicationExpression) + and self.function == other.function + and self.argument == other.argument + ) + + __hash__ = Expression.__hash__ + + def __str__(self): + # uncurry the arguments and find the base function + if self.is_atom(): + function, args = self.uncurry() + arg_str = ",".join("%s" % arg for arg in args) + else: + # Leave arguments curried + function = self.function + arg_str = "%s" % self.argument + + function_str = "%s" % function + parenthesize_function = False + if isinstance(function, LambdaExpression): + if isinstance(function.term, ApplicationExpression): + if not isinstance(function.term.function, AbstractVariableExpression): + parenthesize_function = True + elif not isinstance(function.term, BooleanExpression): + parenthesize_function = True + elif isinstance(function, ApplicationExpression): + parenthesize_function = True + + if parenthesize_function: + function_str = Tokens.OPEN + function_str + Tokens.CLOSE + + return function_str + Tokens.OPEN + arg_str + Tokens.CLOSE + + def uncurry(self): + """ + Uncurry this application expression + + return: A tuple (base-function, arg-list) + """ + function = self.function + args = [self.argument] + while isinstance(function, ApplicationExpression): + # (\x.\y.sees(x,y)(john))(mary) + args.insert(0, function.argument) + function = function.function + return (function, args) + + @property + def pred(self): + """ + Return uncurried base-function. + If this is an atom, then the result will be a variable expression. + Otherwise, it will be a lambda expression. + """ + return self.uncurry()[0] + + @property + def args(self): + """ + Return uncurried arg-list + """ + return self.uncurry()[1] + + def is_atom(self): + """ + Is this expression an atom (as opposed to a lambda expression applied + to a term)? + """ + return isinstance(self.pred, AbstractVariableExpression) + + +@total_ordering +class AbstractVariableExpression(Expression): + """This class represents a variable to be used as a predicate or entity""" + + def __init__(self, variable): + """ + :param variable: ``Variable``, for the variable + """ + assert isinstance(variable, Variable), "%s is not a Variable" % variable + self.variable = variable + + def simplify(self): + return self + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + """:see: Expression.replace()""" + assert isinstance(variable, Variable), "%s is not an Variable" % variable + assert isinstance(expression, Expression), ( + "%s is not an Expression" % expression + ) + if self.variable == variable: + return expression + else: + return self + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + resolution = other_type + for varEx in signature[self.variable.name]: + resolution = varEx.type.resolve(resolution) + if not resolution: + raise InconsistentTypeHierarchyException(self) + + signature[self.variable.name].append(self) + for varEx in signature[self.variable.name]: + varEx.type = resolution + + def findtype(self, variable): + """:see Expression.findtype()""" + assert isinstance(variable, Variable), "%s is not a Variable" % variable + if self.variable == variable: + return self.type + else: + return ANY_TYPE + + def predicates(self): + """:see: Expression.predicates()""" + return set() + + def __eq__(self, other): + """Allow equality between instances of ``AbstractVariableExpression`` + subtypes.""" + return ( + isinstance(other, AbstractVariableExpression) + and self.variable == other.variable + ) + + def __lt__(self, other): + if not isinstance(other, AbstractVariableExpression): + raise TypeError + return self.variable < other.variable + + __hash__ = Expression.__hash__ + + def __str__(self): + return "%s" % self.variable + + +class IndividualVariableExpression(AbstractVariableExpression): + """This class represents variables that take the form of a single lowercase + character (other than 'e') followed by zero or more digits.""" + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + if not other_type.matches(ENTITY_TYPE): + raise IllegalTypeException(self, other_type, ENTITY_TYPE) + + signature[self.variable.name].append(self) + + def _get_type(self): + return ENTITY_TYPE + + type = property(_get_type, _set_type) + + def free(self): + """:see: Expression.free()""" + return {self.variable} + + def constants(self): + """:see: Expression.constants()""" + return set() + + +class FunctionVariableExpression(AbstractVariableExpression): + """This class represents variables that take the form of a single uppercase + character followed by zero or more digits.""" + + type = ANY_TYPE + + def free(self): + """:see: Expression.free()""" + return {self.variable} + + def constants(self): + """:see: Expression.constants()""" + return set() + + +class EventVariableExpression(IndividualVariableExpression): + """This class represents variables that take the form of a single lowercase + 'e' character followed by zero or more digits.""" + + type = EVENT_TYPE + + +class ConstantExpression(AbstractVariableExpression): + """This class represents variables that do not take the form of a single + character followed by zero or more digits.""" + + type = ENTITY_TYPE + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + if other_type == ANY_TYPE: + # entity type by default, for individuals + resolution = ENTITY_TYPE + else: + resolution = other_type + if self.type != ENTITY_TYPE: + resolution = resolution.resolve(self.type) + + for varEx in signature[self.variable.name]: + resolution = varEx.type.resolve(resolution) + if not resolution: + raise InconsistentTypeHierarchyException(self) + + signature[self.variable.name].append(self) + for varEx in signature[self.variable.name]: + varEx.type = resolution + + def free(self): + """:see: Expression.free()""" + return set() + + def constants(self): + """:see: Expression.constants()""" + return {self.variable} + + +def VariableExpression(variable): + """ + This is a factory method that instantiates and returns a subtype of + ``AbstractVariableExpression`` appropriate for the given variable. + """ + assert isinstance(variable, Variable), "%s is not a Variable" % variable + if is_indvar(variable.name): + return IndividualVariableExpression(variable) + elif is_funcvar(variable.name): + return FunctionVariableExpression(variable) + elif is_eventvar(variable.name): + return EventVariableExpression(variable) + else: + return ConstantExpression(variable) + + +class VariableBinderExpression(Expression): + """This an abstract class for any Expression that binds a variable in an + Expression. This includes LambdaExpressions and Quantified Expressions""" + + def __init__(self, variable, term): + """ + :param variable: ``Variable``, for the variable + :param term: ``Expression``, for the term + """ + assert isinstance(variable, Variable), "%s is not a Variable" % variable + assert isinstance(term, Expression), "%s is not an Expression" % term + self.variable = variable + self.term = term + + def replace(self, variable, expression, replace_bound=False, alpha_convert=True): + """:see: Expression.replace()""" + assert isinstance(variable, Variable), "%s is not a Variable" % variable + assert isinstance(expression, Expression), ( + "%s is not an Expression" % expression + ) + # if the bound variable is the thing being replaced + if self.variable == variable: + if replace_bound: + assert isinstance(expression, AbstractVariableExpression), ( + "%s is not a AbstractVariableExpression" % expression + ) + return self.__class__( + expression.variable, + self.term.replace(variable, expression, True, alpha_convert), + ) + else: + return self + else: + # if the bound variable appears in the expression, then it must + # be alpha converted to avoid a conflict + if alpha_convert and self.variable in expression.free(): + self = self.alpha_convert(unique_variable(pattern=self.variable)) + + # replace in the term + return self.__class__( + self.variable, + self.term.replace(variable, expression, replace_bound, alpha_convert), + ) + + def alpha_convert(self, newvar): + """Rename all occurrences of the variable introduced by this variable + binder in the expression to ``newvar``. + :param newvar: ``Variable``, for the new variable + """ + assert isinstance(newvar, Variable), "%s is not a Variable" % newvar + return self.__class__( + newvar, self.term.replace(self.variable, VariableExpression(newvar), True) + ) + + def free(self): + """:see: Expression.free()""" + return self.term.free() - {self.variable} + + def findtype(self, variable): + """:see Expression.findtype()""" + assert isinstance(variable, Variable), "%s is not a Variable" % variable + if variable == self.variable: + return ANY_TYPE + else: + return self.term.findtype(variable) + + def visit(self, function, combinator): + """:see: Expression.visit()""" + return combinator([function(self.term)]) + + def visit_structured(self, function, combinator): + """:see: Expression.visit_structured()""" + return combinator(self.variable, function(self.term)) + + def __eq__(self, other): + r"""Defines equality modulo alphabetic variance. If we are comparing + \x.M and \y.N, then check equality of M and N[x/y].""" + if isinstance(self, other.__class__) or isinstance(other, self.__class__): + if self.variable == other.variable: + return self.term == other.term + else: + # Comparing \x.M and \y.N. Relabel y in N with x and continue. + varex = VariableExpression(self.variable) + return self.term == other.term.replace(other.variable, varex) + else: + return False + + __hash__ = Expression.__hash__ + + +class LambdaExpression(VariableBinderExpression): + @property + def type(self): + return ComplexType(self.term.findtype(self.variable), self.term.type) + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + self.term._set_type(other_type.second, signature) + if not self.type.resolve(other_type): + raise TypeResolutionException(self, other_type) + + def __str__(self): + variables = [self.variable] + term = self.term + while term.__class__ == self.__class__: + variables.append(term.variable) + term = term.term + return ( + Tokens.LAMBDA + + " ".join("%s" % v for v in variables) + + Tokens.DOT + + "%s" % term + ) + + +class QuantifiedExpression(VariableBinderExpression): + @property + def type(self): + return TRUTH_TYPE + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + if not other_type.matches(TRUTH_TYPE): + raise IllegalTypeException(self, other_type, TRUTH_TYPE) + self.term._set_type(TRUTH_TYPE, signature) + + def __str__(self): + variables = [self.variable] + term = self.term + while term.__class__ == self.__class__: + variables.append(term.variable) + term = term.term + return ( + self.getQuantifier() + + " " + + " ".join("%s" % v for v in variables) + + Tokens.DOT + + "%s" % term + ) + + +class ExistsExpression(QuantifiedExpression): + def getQuantifier(self): + return Tokens.EXISTS + + +class AllExpression(QuantifiedExpression): + def getQuantifier(self): + return Tokens.ALL + + +class IotaExpression(QuantifiedExpression): + def getQuantifier(self): + return Tokens.IOTA + + +class NegatedExpression(Expression): + def __init__(self, term): + assert isinstance(term, Expression), "%s is not an Expression" % term + self.term = term + + @property + def type(self): + return TRUTH_TYPE + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + if not other_type.matches(TRUTH_TYPE): + raise IllegalTypeException(self, other_type, TRUTH_TYPE) + self.term._set_type(TRUTH_TYPE, signature) + + def findtype(self, variable): + assert isinstance(variable, Variable), "%s is not a Variable" % variable + return self.term.findtype(variable) + + def visit(self, function, combinator): + """:see: Expression.visit()""" + return combinator([function(self.term)]) + + def negate(self): + """:see: Expression.negate()""" + return self.term + + def __eq__(self, other): + return isinstance(other, NegatedExpression) and self.term == other.term + + __hash__ = Expression.__hash__ + + def __str__(self): + return Tokens.NOT + "%s" % self.term + + +class BinaryExpression(Expression): + def __init__(self, first, second): + assert isinstance(first, Expression), "%s is not an Expression" % first + assert isinstance(second, Expression), "%s is not an Expression" % second + self.first = first + self.second = second + + @property + def type(self): + return TRUTH_TYPE + + def findtype(self, variable): + """:see Expression.findtype()""" + assert isinstance(variable, Variable), "%s is not a Variable" % variable + f = self.first.findtype(variable) + s = self.second.findtype(variable) + if f == s or s == ANY_TYPE: + return f + elif f == ANY_TYPE: + return s + else: + return ANY_TYPE + + def visit(self, function, combinator): + """:see: Expression.visit()""" + return combinator([function(self.first), function(self.second)]) + + def __eq__(self, other): + return ( + (isinstance(self, other.__class__) or isinstance(other, self.__class__)) + and self.first == other.first + and self.second == other.second + ) + + __hash__ = Expression.__hash__ + + def __str__(self): + first = self._str_subex(self.first) + second = self._str_subex(self.second) + return Tokens.OPEN + first + " " + self.getOp() + " " + second + Tokens.CLOSE + + def _str_subex(self, subex): + return "%s" % subex + + +class BooleanExpression(BinaryExpression): + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + if not other_type.matches(TRUTH_TYPE): + raise IllegalTypeException(self, other_type, TRUTH_TYPE) + self.first._set_type(TRUTH_TYPE, signature) + self.second._set_type(TRUTH_TYPE, signature) + + +class AndExpression(BooleanExpression): + """This class represents conjunctions""" + + def getOp(self): + return Tokens.AND + + def _str_subex(self, subex): + s = "%s" % subex + if isinstance(subex, AndExpression): + return s[1:-1] + return s + + +class OrExpression(BooleanExpression): + """This class represents disjunctions""" + + def getOp(self): + return Tokens.OR + + def _str_subex(self, subex): + s = "%s" % subex + if isinstance(subex, OrExpression): + return s[1:-1] + return s + + +class ImpExpression(BooleanExpression): + """This class represents implications""" + + def getOp(self): + return Tokens.IMP + + +class IffExpression(BooleanExpression): + """This class represents biconditionals""" + + def getOp(self): + return Tokens.IFF + + +class EqualityExpression(BinaryExpression): + """This class represents equality expressions like "(x = y)".""" + + def _set_type(self, other_type=ANY_TYPE, signature=None): + """:see Expression._set_type()""" + assert isinstance(other_type, Type) + + if signature is None: + signature = defaultdict(list) + + if not other_type.matches(TRUTH_TYPE): + raise IllegalTypeException(self, other_type, TRUTH_TYPE) + self.first._set_type(ENTITY_TYPE, signature) + self.second._set_type(ENTITY_TYPE, signature) + + def getOp(self): + return Tokens.EQ + + +### Utilities + + +class LogicalExpressionException(Exception): + def __init__(self, index, message): + self.index = index + Exception.__init__(self, message) + + +class UnexpectedTokenException(LogicalExpressionException): + def __init__(self, index, unexpected=None, expected=None, message=None): + if unexpected and expected: + msg = "Unexpected token: '%s'. " "Expected token '%s'." % ( + unexpected, + expected, + ) + elif unexpected: + msg = "Unexpected token: '%s'." % unexpected + if message: + msg += " " + message + else: + msg = "Expected token '%s'." % expected + LogicalExpressionException.__init__(self, index, msg) + + +class ExpectedMoreTokensException(LogicalExpressionException): + def __init__(self, index, message=None): + if not message: + message = "More tokens expected." + LogicalExpressionException.__init__( + self, index, "End of input found. " + message + ) + + +def is_indvar(expr): + """ + An individual variable must be a single lowercase character other than 'e', + followed by zero or more digits. + + :param expr: str + :return: bool True if expr is of the correct form + """ + assert isinstance(expr, str), "%s is not a string" % expr + return re.match(r"^[a-df-z]\d*$", expr) is not None + + +def is_funcvar(expr): + """ + A function variable must be a single uppercase character followed by + zero or more digits. + + :param expr: str + :return: bool True if expr is of the correct form + """ + assert isinstance(expr, str), "%s is not a string" % expr + return re.match(r"^[A-Z]\d*$", expr) is not None + + +def is_eventvar(expr): + """ + An event variable must be a single lowercase 'e' character followed by + zero or more digits. + + :param expr: str + :return: bool True if expr is of the correct form + """ + assert isinstance(expr, str), "%s is not a string" % expr + return re.match(r"^e\d*$", expr) is not None + + +def demo(): + lexpr = Expression.fromstring + print("=" * 20 + "Test reader" + "=" * 20) + print(lexpr(r"john")) + print(lexpr(r"man(x)")) + print(lexpr(r"-man(x)")) + print(lexpr(r"(man(x) & tall(x) & walks(x))")) + print(lexpr(r"exists x.(man(x) & tall(x) & walks(x))")) + print(lexpr(r"\x.man(x)")) + print(lexpr(r"\x.man(x)(john)")) + print(lexpr(r"\x y.sees(x,y)")) + print(lexpr(r"\x y.sees(x,y)(a,b)")) + print(lexpr(r"(\x.exists y.walks(x,y))(x)")) + print(lexpr(r"exists x.x = y")) + print(lexpr(r"exists x.(x = y)")) + print(lexpr("P(x) & x=y & P(y)")) + print(lexpr(r"\P Q.exists x.(P(x) & Q(x))")) + print(lexpr(r"man(x) <-> tall(x)")) + + print("=" * 20 + "Test simplify" + "=" * 20) + print(lexpr(r"\x.\y.sees(x,y)(john)(mary)").simplify()) + print(lexpr(r"\x.\y.sees(x,y)(john, mary)").simplify()) + print(lexpr(r"all x.(man(x) & (\x.exists y.walks(x,y))(x))").simplify()) + print(lexpr(r"(\P.\Q.exists x.(P(x) & Q(x)))(\x.dog(x))(\x.bark(x))").simplify()) + + print("=" * 20 + "Test alpha conversion and binder expression equality" + "=" * 20) + e1 = lexpr("exists x.P(x)") + print(e1) + e2 = e1.alpha_convert(Variable("z")) + print(e2) + print(e1 == e2) + + +def demo_errors(): + print("=" * 20 + "Test reader errors" + "=" * 20) + demoException("(P(x) & Q(x)") + demoException("((P(x) &) & Q(x))") + demoException("P(x) -> ") + demoException("P(x") + demoException("P(x,") + demoException("P(x,)") + demoException("exists") + demoException("exists x.") + demoException("\\") + demoException("\\ x y.") + demoException("P(x)Q(x)") + demoException("(P(x)Q(x)") + demoException("exists x -> y") + + +def demoException(s): + try: + Expression.fromstring(s) + except LogicalExpressionException as e: + print(f"{e.__class__.__name__}: {e}") + + +def printtype(ex): + print(f"{ex.str()} : {ex.type}") + + +if __name__ == "__main__": + demo() +# demo_errors() diff --git a/nltk/sem/relextract.py b/nltk/sem/relextract.py new file mode 100644 index 0000000..3b9533c --- /dev/null +++ b/nltk/sem/relextract.py @@ -0,0 +1,539 @@ +# Natural Language Toolkit: Relation Extraction +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +""" +Code for extracting relational triples from the ieer and conll2002 corpora. + +Relations are stored internally as dictionaries ('reldicts'). + +The two serialization outputs are "rtuple" and "clause". + +- An rtuple is a tuple of the form ``(subj, filler, obj)``, + where ``subj`` and ``obj`` are pairs of Named Entity mentions, and ``filler`` is the string of words + occurring between ``sub`` and ``obj`` (with no intervening NEs). Strings are printed via ``repr()`` to + circumvent locale variations in rendering utf-8 encoded strings. +- A clause is an atom of the form ``relsym(subjsym, objsym)``, + where the relation, subject and object have been canonicalized to single strings. +""" + +# todo: get a more general solution to canonicalized symbols for clauses -- maybe use xmlcharrefs? + +import html +import re +from collections import defaultdict + +# Dictionary that associates corpora with NE classes +NE_CLASSES = { + "ieer": [ + "LOCATION", + "ORGANIZATION", + "PERSON", + "DURATION", + "DATE", + "CARDINAL", + "PERCENT", + "MONEY", + "MEASURE", + ], + "conll2002": ["LOC", "PER", "ORG"], + "ace": [ + "LOCATION", + "ORGANIZATION", + "PERSON", + "DURATION", + "DATE", + "CARDINAL", + "PERCENT", + "MONEY", + "MEASURE", + "FACILITY", + "GPE", + ], +} + +# Allow abbreviated class labels +short2long = dict(LOC="LOCATION", ORG="ORGANIZATION", PER="PERSON") +long2short = dict(LOCATION="LOC", ORGANIZATION="ORG", PERSON="PER") + + +def _expand(type): + """ + Expand an NE class name. + :type type: str + :rtype: str + """ + try: + return short2long[type] + except KeyError: + return type + + +def class_abbrev(type): + """ + Abbreviate an NE class name. + :type type: str + :rtype: str + """ + try: + return long2short[type] + except KeyError: + return type + + +def _join(lst, sep=" ", untag=False): + """ + Join a list into a string, turning tags tuples into tag strings or just words. + :param untag: if ``True``, omit the tag from tagged input strings. + :type lst: list + :rtype: str + """ + try: + return sep.join(lst) + except TypeError: + if untag: + return sep.join(tup[0] for tup in lst) + from nltk.tag import tuple2str + + return sep.join(tuple2str(tup) for tup in lst) + + +def descape_entity(m, defs=html.entities.entitydefs): + """ + Translate one entity to its ISO Latin value. + Inspired by example from effbot.org + + + """ + try: + return defs[m.group(1)] + + except KeyError: + return m.group(0) # use as is + + +def list2sym(lst): + """ + Convert a list of strings into a canonical symbol. + :type lst: list + :return: a Unicode string without whitespace + :rtype: unicode + """ + sym = _join(lst, "_", untag=True) + sym = sym.lower() + ENT = re.compile(r"&(\w+?);") + sym = ENT.sub(descape_entity, sym) + sym = sym.replace(".", "") + return sym + + +def tree2semi_rel(tree): + """ + Group a chunk structure into a list of 'semi-relations' of the form (list(str), ``Tree``). + + In order to facilitate the construction of (``Tree``, string, ``Tree``) triples, this + identifies pairs whose first member is a list (possibly empty) of terminal + strings, and whose second member is a ``Tree`` of the form (NE_label, terminals). + + :param tree: a chunk tree + :return: a list of pairs (list(str), ``Tree``) + :rtype: list of tuple + """ + + from nltk.tree import Tree + + semi_rels = [] + semi_rel = [[], None] + + for dtr in tree: + if not isinstance(dtr, Tree): + semi_rel[0].append(dtr) + else: + # dtr is a Tree + semi_rel[1] = dtr + semi_rels.append(semi_rel) + semi_rel = [[], None] + return semi_rels + + +def semi_rel2reldict(pairs, window=5, trace=False): + """ + Converts the pairs generated by ``tree2semi_rel`` into a 'reldict': a dictionary which + stores information about the subject and object NEs plus the filler between them. + Additionally, a left and right context of length =< window are captured (within + a given input sentence). + + :param pairs: a pair of list(str) and ``Tree``, as generated by + :param window: a threshold for the number of items to include in the left and right context + :type window: int + :return: 'relation' dictionaries whose keys are 'lcon', 'subjclass', 'subjtext', 'subjsym', 'filler', objclass', objtext', 'objsym' and 'rcon' + :rtype: list(defaultdict) + """ + result = [] + while len(pairs) > 2: + reldict = defaultdict(str) + reldict["lcon"] = _join(pairs[0][0][-window:]) + reldict["subjclass"] = pairs[0][1].label() + reldict["subjtext"] = _join(pairs[0][1].leaves()) + reldict["subjsym"] = list2sym(pairs[0][1].leaves()) + reldict["filler"] = _join(pairs[1][0]) + reldict["untagged_filler"] = _join(pairs[1][0], untag=True) + reldict["objclass"] = pairs[1][1].label() + reldict["objtext"] = _join(pairs[1][1].leaves()) + reldict["objsym"] = list2sym(pairs[1][1].leaves()) + reldict["rcon"] = _join(pairs[2][0][:window]) + if trace: + print( + "(%s(%s, %s)" + % ( + reldict["untagged_filler"], + reldict["subjclass"], + reldict["objclass"], + ) + ) + result.append(reldict) + pairs = pairs[1:] + return result + + +def extract_rels(subjclass, objclass, doc, corpus="ace", pattern=None, window=10): + """ + Filter the output of ``semi_rel2reldict`` according to specified NE classes and a filler pattern. + + The parameters ``subjclass`` and ``objclass`` can be used to restrict the + Named Entities to particular types (any of 'LOCATION', 'ORGANIZATION', + 'PERSON', 'DURATION', 'DATE', 'CARDINAL', 'PERCENT', 'MONEY', 'MEASURE'). + + :param subjclass: the class of the subject Named Entity. + :type subjclass: str + :param objclass: the class of the object Named Entity. + :type objclass: str + :param doc: input document + :type doc: ieer document or a list of chunk trees + :param corpus: name of the corpus to take as input; possible values are + 'ieer' and 'conll2002' + :type corpus: str + :param pattern: a regular expression for filtering the fillers of + retrieved triples. + :type pattern: SRE_Pattern + :param window: filters out fillers which exceed this threshold + :type window: int + :return: see ``mk_reldicts`` + :rtype: list(defaultdict) + """ + + if subjclass and subjclass not in NE_CLASSES[corpus]: + if _expand(subjclass) in NE_CLASSES[corpus]: + subjclass = _expand(subjclass) + else: + raise ValueError( + "your value for the subject type has not been recognized: %s" + % subjclass + ) + if objclass and objclass not in NE_CLASSES[corpus]: + if _expand(objclass) in NE_CLASSES[corpus]: + objclass = _expand(objclass) + else: + raise ValueError( + "your value for the object type has not been recognized: %s" % objclass + ) + + if corpus == "ace" or corpus == "conll2002": + pairs = tree2semi_rel(doc) + elif corpus == "ieer": + pairs = tree2semi_rel(doc.text) + tree2semi_rel(doc.headline) + else: + raise ValueError("corpus type not recognized") + + reldicts = semi_rel2reldict(pairs) + + relfilter = lambda x: ( + x["subjclass"] == subjclass + and len(x["filler"].split()) <= window + and pattern.match(x["filler"]) + and x["objclass"] == objclass + ) + + return list(filter(relfilter, reldicts)) + + +def rtuple(reldict, lcon=False, rcon=False): + """ + Pretty print the reldict as an rtuple. + :param reldict: a relation dictionary + :type reldict: defaultdict + """ + items = [ + class_abbrev(reldict["subjclass"]), + reldict["subjtext"], + reldict["filler"], + class_abbrev(reldict["objclass"]), + reldict["objtext"], + ] + format = "[%s: %r] %r [%s: %r]" + if lcon: + items = [reldict["lcon"]] + items + format = "...%r)" + format + if rcon: + items.append(reldict["rcon"]) + format = format + "(%r..." + printargs = tuple(items) + return format % printargs + + +def clause(reldict, relsym): + """ + Print the relation in clausal form. + :param reldict: a relation dictionary + :type reldict: defaultdict + :param relsym: a label for the relation + :type relsym: str + """ + items = (relsym, reldict["subjsym"], reldict["objsym"]) + return "%s(%r, %r)" % items + + +####################################################### +# Demos of relation extraction with regular expressions +####################################################### + + +############################################ +# Example of in(ORG, LOC) +############################################ +def in_demo(trace=0, sql=True): + """ + Select pairs of organizations and locations whose mentions occur with an + intervening occurrence of the preposition "in". + + If the sql parameter is set to True, then the entity pairs are loaded into + an in-memory database, and subsequently pulled out using an SQL "SELECT" + query. + """ + from nltk.corpus import ieer + + if sql: + try: + import sqlite3 + + connection = sqlite3.connect(":memory:") + cur = connection.cursor() + cur.execute( + """create table Locations + (OrgName text, LocationName text, DocID text)""" + ) + except ImportError: + import warnings + + warnings.warn("Cannot import sqlite; sql flag will be ignored.") + + IN = re.compile(r".*\bin\b(?!\b.+ing)") + + print() + print("IEER: in(ORG, LOC) -- just the clauses:") + print("=" * 45) + + for file in ieer.fileids(): + for doc in ieer.parsed_docs(file): + if trace: + print(doc.docno) + print("=" * 15) + for rel in extract_rels("ORG", "LOC", doc, corpus="ieer", pattern=IN): + print(clause(rel, relsym="IN")) + if sql: + try: + rtuple = (rel["subjtext"], rel["objtext"], doc.docno) + cur.execute( + """insert into Locations + values (?, ?, ?)""", + rtuple, + ) + connection.commit() + except NameError: + pass + + if sql: + try: + cur.execute( + """select OrgName from Locations + where LocationName = 'Atlanta'""" + ) + print() + print("Extract data from SQL table: ORGs in Atlanta") + print("-" * 15) + for row in cur: + print(row) + except NameError: + pass + + +############################################ +# Example of has_role(PER, LOC) +############################################ + + +def roles_demo(trace=0): + from nltk.corpus import ieer + + roles = r""" + (.*( # assorted roles + analyst| + chair(wo)?man| + commissioner| + counsel| + director| + economist| + editor| + executive| + foreman| + governor| + head| + lawyer| + leader| + librarian).*)| + manager| + partner| + president| + producer| + professor| + researcher| + spokes(wo)?man| + writer| + ,\sof\sthe?\s* # "X, of (the) Y" + """ + ROLES = re.compile(roles, re.VERBOSE) + + print() + print("IEER: has_role(PER, ORG) -- raw rtuples:") + print("=" * 45) + + for file in ieer.fileids(): + for doc in ieer.parsed_docs(file): + lcon = rcon = False + if trace: + print(doc.docno) + print("=" * 15) + lcon = rcon = True + for rel in extract_rels("PER", "ORG", doc, corpus="ieer", pattern=ROLES): + print(rtuple(rel, lcon=lcon, rcon=rcon)) + + +############################################## +### Show what's in the IEER Headlines +############################################## + + +def ieer_headlines(): + from nltk.corpus import ieer + from nltk.tree import Tree + + print("IEER: First 20 Headlines") + print("=" * 45) + + trees = [ + (doc.docno, doc.headline) + for file in ieer.fileids() + for doc in ieer.parsed_docs(file) + ] + for tree in trees[:20]: + print() + print("%s:\n%s" % tree) + + +############################################# +## Dutch CONLL2002: take_on_role(PER, ORG +############################################# + + +def conllned(trace=1): + """ + Find the copula+'van' relation ('of') in the Dutch tagged training corpus + from CoNLL 2002. + """ + + from nltk.corpus import conll2002 + + vnv = """ + ( + is/V| # 3rd sing present and + was/V| # past forms of the verb zijn ('be') + werd/V| # and also present + wordt/V # past of worden ('become) + ) + .* # followed by anything + van/Prep # followed by van ('of') + """ + VAN = re.compile(vnv, re.VERBOSE) + + print() + print("Dutch CoNLL2002: van(PER, ORG) -- raw rtuples with context:") + print("=" * 45) + + for doc in conll2002.chunked_sents("ned.train"): + lcon = rcon = False + if trace: + lcon = rcon = True + for rel in extract_rels( + "PER", "ORG", doc, corpus="conll2002", pattern=VAN, window=10 + ): + print(rtuple(rel, lcon=lcon, rcon=rcon)) + + +############################################# +## Spanish CONLL2002: (PER, ORG) +############################################# + + +def conllesp(): + from nltk.corpus import conll2002 + + de = """ + .* + ( + de/SP| + del/SP + ) + """ + DE = re.compile(de, re.VERBOSE) + + print() + print("Spanish CoNLL2002: de(ORG, LOC) -- just the first 10 clauses:") + print("=" * 45) + rels = [ + rel + for doc in conll2002.chunked_sents("esp.train") + for rel in extract_rels("ORG", "LOC", doc, corpus="conll2002", pattern=DE) + ] + for r in rels[:10]: + print(clause(r, relsym="DE")) + print() + + +def ne_chunked(): + print() + print("1500 Sentences from Penn Treebank, as processed by NLTK NE Chunker") + print("=" * 45) + ROLE = re.compile( + r".*(chairman|president|trader|scientist|economist|analyst|partner).*" + ) + rels = [] + for i, sent in enumerate(nltk.corpus.treebank.tagged_sents()[:1500]): + sent = nltk.ne_chunk(sent) + rels = extract_rels("PER", "ORG", sent, corpus="ace", pattern=ROLE, window=7) + for rel in rels: + print(f"{i:<5}{rtuple(rel)}") + + +if __name__ == "__main__": + import nltk + from nltk.sem import relextract + + in_demo(trace=0) + roles_demo(trace=0) + conllned() + conllesp() + ieer_headlines() + ne_chunked() diff --git a/nltk/sem/skolemize.py b/nltk/sem/skolemize.py new file mode 100644 index 0000000..58cddef --- /dev/null +++ b/nltk/sem/skolemize.py @@ -0,0 +1,148 @@ +# Natural Language Toolkit: Semantic Interpretation +# +# Author: Ewan Klein +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +from nltk.sem.logic import ( + AllExpression, + AndExpression, + ApplicationExpression, + EqualityExpression, + ExistsExpression, + IffExpression, + ImpExpression, + NegatedExpression, + OrExpression, + VariableExpression, + skolem_function, + unique_variable, +) + + +def skolemize(expression, univ_scope=None, used_variables=None): + """ + Skolemize the expression and convert to conjunctive normal form (CNF) + """ + if univ_scope is None: + univ_scope = set() + if used_variables is None: + used_variables = set() + + if isinstance(expression, AllExpression): + term = skolemize( + expression.term, + univ_scope | {expression.variable}, + used_variables | {expression.variable}, + ) + return term.replace( + expression.variable, + VariableExpression(unique_variable(ignore=used_variables)), + ) + elif isinstance(expression, AndExpression): + return skolemize(expression.first, univ_scope, used_variables) & skolemize( + expression.second, univ_scope, used_variables + ) + elif isinstance(expression, OrExpression): + return to_cnf( + skolemize(expression.first, univ_scope, used_variables), + skolemize(expression.second, univ_scope, used_variables), + ) + elif isinstance(expression, ImpExpression): + return to_cnf( + skolemize(-expression.first, univ_scope, used_variables), + skolemize(expression.second, univ_scope, used_variables), + ) + elif isinstance(expression, IffExpression): + return to_cnf( + skolemize(-expression.first, univ_scope, used_variables), + skolemize(expression.second, univ_scope, used_variables), + ) & to_cnf( + skolemize(expression.first, univ_scope, used_variables), + skolemize(-expression.second, univ_scope, used_variables), + ) + elif isinstance(expression, EqualityExpression): + return expression + elif isinstance(expression, NegatedExpression): + negated = expression.term + if isinstance(negated, AllExpression): + term = skolemize( + -negated.term, univ_scope, used_variables | {negated.variable} + ) + if univ_scope: + return term.replace(negated.variable, skolem_function(univ_scope)) + else: + skolem_constant = VariableExpression( + unique_variable(ignore=used_variables) + ) + return term.replace(negated.variable, skolem_constant) + elif isinstance(negated, AndExpression): + return to_cnf( + skolemize(-negated.first, univ_scope, used_variables), + skolemize(-negated.second, univ_scope, used_variables), + ) + elif isinstance(negated, OrExpression): + return skolemize(-negated.first, univ_scope, used_variables) & skolemize( + -negated.second, univ_scope, used_variables + ) + elif isinstance(negated, ImpExpression): + return skolemize(negated.first, univ_scope, used_variables) & skolemize( + -negated.second, univ_scope, used_variables + ) + elif isinstance(negated, IffExpression): + return to_cnf( + skolemize(-negated.first, univ_scope, used_variables), + skolemize(-negated.second, univ_scope, used_variables), + ) & to_cnf( + skolemize(negated.first, univ_scope, used_variables), + skolemize(negated.second, univ_scope, used_variables), + ) + elif isinstance(negated, EqualityExpression): + return expression + elif isinstance(negated, NegatedExpression): + return skolemize(negated.term, univ_scope, used_variables) + elif isinstance(negated, ExistsExpression): + term = skolemize( + -negated.term, + univ_scope | {negated.variable}, + used_variables | {negated.variable}, + ) + return term.replace( + negated.variable, + VariableExpression(unique_variable(ignore=used_variables)), + ) + elif isinstance(negated, ApplicationExpression): + return expression + else: + raise Exception("'%s' cannot be skolemized" % expression) + elif isinstance(expression, ExistsExpression): + term = skolemize( + expression.term, univ_scope, used_variables | {expression.variable} + ) + if univ_scope: + return term.replace(expression.variable, skolem_function(univ_scope)) + else: + skolem_constant = VariableExpression(unique_variable(ignore=used_variables)) + return term.replace(expression.variable, skolem_constant) + elif isinstance(expression, ApplicationExpression): + return expression + else: + raise Exception("'%s' cannot be skolemized" % expression) + + +def to_cnf(first, second): + """ + Convert this split disjunction to conjunctive normal form (CNF) + """ + if isinstance(first, AndExpression): + r_first = to_cnf(first.first, second) + r_second = to_cnf(first.second, second) + return r_first & r_second + elif isinstance(second, AndExpression): + r_first = to_cnf(first, second.first) + r_second = to_cnf(first, second.second) + return r_first & r_second + else: + return first | second diff --git a/nltk/sem/util.py b/nltk/sem/util.py new file mode 100644 index 0000000..95c2457 --- /dev/null +++ b/nltk/sem/util.py @@ -0,0 +1,311 @@ +# Natural Language Toolkit: Semantic Interpretation +# +# Author: Ewan Klein +# +# Copyright (C) 2001-2026 NLTK Project +# URL: +# For license information, see LICENSE.TXT + +""" +Utility functions for batch-processing sentences: parsing and +extraction of the semantic representation of the root node of the the +syntax tree, followed by evaluation of the semantic representation in +a first-order model. +""" + +from nltk.pathsec import open as _secure_open +from nltk.sem import evaluate + +############################################################## +## Utility functions for connecting parse output to semantics +############################################################## + + +def parse_sents(inputs, grammar, trace=0): + """ + Convert input sentences into syntactic trees. + + :param inputs: sentences to be parsed + :type inputs: list(str) + :param grammar: ``FeatureGrammar`` or name of feature-based grammar + :type grammar: nltk.grammar.FeatureGrammar + :rtype: list(nltk.tree.Tree) or dict(list(str)): list(Tree) + :return: a mapping from input sentences to a list of ``Tree`` instances. + """ + # put imports here to avoid circult dependencies + from nltk.grammar import FeatureGrammar + from nltk.parse import FeatureChartParser, load_parser + + if isinstance(grammar, FeatureGrammar): + cp = FeatureChartParser(grammar) + else: + cp = load_parser(grammar, trace=trace) + parses = [] + for sent in inputs: + tokens = sent.split() # use a tokenizer? + syntrees = list(cp.parse(tokens)) + parses.append(syntrees) + return parses + + +def root_semrep(syntree, semkey="SEM"): + """ + Find the semantic representation at the root of a tree. + + :param syntree: a parse ``Tree`` + :param semkey: the feature label to use for the root semantics in the tree + :return: the semantic representation at the root of a ``Tree`` + :rtype: sem.Expression + """ + from nltk.grammar import FeatStructNonterminal + + node = syntree.label() + assert isinstance(node, FeatStructNonterminal) + try: + return node[semkey] + except KeyError: + print(node, end=" ") + print("has no specification for the feature %s" % semkey) + raise + + +def interpret_sents(inputs, grammar, semkey="SEM", trace=0): + """ + Add the semantic representation to each syntactic parse tree + of each input sentence. + + :param inputs: a list of sentences + :type inputs: list(str) + :param grammar: ``FeatureGrammar`` or name of feature-based grammar + :type grammar: nltk.grammar.FeatureGrammar + :return: a mapping from sentences to lists of pairs (parse-tree, semantic-representations) + :rtype: list(list(tuple(nltk.tree.Tree, nltk.sem.logic.ConstantExpression))) + """ + return [ + [(syn, root_semrep(syn, semkey)) for syn in syntrees] + for syntrees in parse_sents(inputs, grammar, trace=trace) + ] + + +def evaluate_sents(inputs, grammar, model, assignment, trace=0): + """ + Add the truth-in-a-model value to each semantic representation + for each syntactic parse of each input sentences. + + :param inputs: a list of sentences + :type inputs: list(str) + :param grammar: ``FeatureGrammar`` or name of feature-based grammar + :type grammar: nltk.grammar.FeatureGrammar + :return: a mapping from sentences to lists of triples (parse-tree, semantic-representations, evaluation-in-model) + :rtype: list(list(tuple(nltk.tree.Tree, nltk.sem.logic.ConstantExpression, bool or dict(str): bool))) + """ + return [ + [ + (syn, sem, model.evaluate("%s" % sem, assignment, trace=trace)) + for (syn, sem) in interpretations + ] + for interpretations in interpret_sents(inputs, grammar) + ] + + +def demo_model0(): + global m0, g0 + # Initialize a valuation of non-logical constants.""" + v = [ + ("john", "b1"), + ("mary", "g1"), + ("suzie", "g2"), + ("fido", "d1"), + ("tess", "d2"), + ("noosa", "n"), + ("girl", {"g1", "g2"}), + ("boy", {"b1", "b2"}), + ("dog", {"d1", "d2"}), + ("bark", {"d1", "d2"}), + ("walk", {"b1", "g2", "d1"}), + ("chase", {("b1", "g1"), ("b2", "g1"), ("g1", "d1"), ("g2", "d2")}), + ( + "see", + {("b1", "g1"), ("b2", "d2"), ("g1", "b1"), ("d2", "b1"), ("g2", "n")}, + ), + ("in", {("b1", "n"), ("b2", "n"), ("d2", "n")}), + ("with", {("b1", "g1"), ("g1", "b1"), ("d1", "b1"), ("b1", "d1")}), + ] + # Read in the data from ``v`` + val = evaluate.Valuation(v) + # Bind ``dom`` to the ``domain`` property of ``val`` + dom = val.domain + # Initialize a model with parameters ``dom`` and ``val``. + m0 = evaluate.Model(dom, val) + # Initialize a variable assignment with parameter ``dom`` + g0 = evaluate.Assignment(dom) + + +def read_sents(filename, encoding="utf8"): + # Reroute to secure sentinel for path validation + with _secure_open(filename, "r", encoding=encoding) as fp: + sents = [l.rstrip() for l in fp] + + # Filter out blank lines and comments + sents = [l for l in sents if len(l) > 0] + sents = [l for l in sents if not l[0] == "#"] + + return sents + + +def demo_legacy_grammar(): + """ + Check that interpret_sents() is compatible with legacy grammars that use + a lowercase 'sem' feature. + + Define 'test.fcfg' to be the following + + """ + from nltk.grammar import FeatureGrammar + + g = FeatureGrammar.fromstring( + """ + % start S + S[sem=] -> 'hello' + """ + ) + print("Reading grammar: %s" % g) + print("*" * 20) + for reading in interpret_sents(["hello"], g, semkey="sem"): + syn, sem = reading[0] + print() + print("output: ", sem) + + +def demo(): + import sys + from optparse import OptionParser + + description = """ + Parse and evaluate some sentences. + """ + + opts = OptionParser(description=description) + + opts.set_defaults( + evaluate=True, + beta=True, + syntrace=0, + semtrace=0, + demo="default", + grammar="", + sentences="", + ) + + opts.add_option( + "-d", + "--demo", + dest="demo", + help="choose demo D; omit this for the default demo, or specify 'chat80'", + metavar="D", + ) + opts.add_option( + "-g", "--gram", dest="grammar", help="read in grammar G", metavar="G" + ) + opts.add_option( + "-m", + "--model", + dest="model", + help="import model M (omit '.py' suffix)", + metavar="M", + ) + opts.add_option( + "-s", + "--sentences", + dest="sentences", + help="read in a file of test sentences S", + metavar="S", + ) + opts.add_option( + "-e", + "--no-eval", + action="store_false", + dest="evaluate", + help="just do a syntactic analysis", + ) + opts.add_option( + "-b", + "--no-beta-reduction", + action="store_false", + dest="beta", + help="don't carry out beta-reduction", + ) + opts.add_option( + "-t", + "--syntrace", + action="count", + dest="syntrace", + help="set syntactic tracing on; requires '-e' option", + ) + opts.add_option( + "-T", + "--semtrace", + action="count", + dest="semtrace", + help="set semantic tracing on", + ) + + (options, args) = opts.parse_args() + + SPACER = "-" * 30 + + demo_model0() + + sents = [ + "Fido sees a boy with Mary", + "John sees Mary", + "every girl chases a dog", + "every boy chases a girl", + "John walks with a girl in Noosa", + "who walks", + ] + + gramfile = "grammars/sample_grammars/sem2.fcfg" + + if options.sentences: + sentsfile = options.sentences + if options.grammar: + gramfile = options.grammar + + if options.model: + opts.error( + "--model is currently unsupported in demo(); the CLI always uses the built-in demo model" + ) + + if sents is None: + sents = read_sents(sentsfile) + + # Set model and assignment + model = m0 + g = g0 + + if options.evaluate: + evaluations = evaluate_sents(sents, gramfile, model, g, trace=options.semtrace) + else: + semreps = interpret_sents(sents, gramfile, trace=options.syntrace) + + for i, sent in enumerate(sents): + n = 1 + print("\nSentence: %s" % sent) + print(SPACER) + if options.evaluate: + for syntree, semrep, value in evaluations[i]: + if isinstance(value, dict): + value = set(value.keys()) + print("%d: %s" % (n, semrep)) + print(value) + n += 1 + else: + for syntree, semrep in semreps[i]: + print("%d: %s" % (n, semrep)) + n += 1 + + +if __name__ == "__main__": + demo() + demo_legacy_grammar() diff --git a/nltk/sentiment/__init__.py b/nltk/sentiment/__init__.py new file mode 100644 index 0000000..6e4491f --- /dev/null +++ b/nltk/sentiment/__init__.py @@ -0,0 +1,13 @@ +# Natural Language Toolkit: Sentiment Analysis +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Ewan Klein +# URL: +# For license information, see LICENSE.TXT + +""" +NLTK Sentiment Analysis Package + +""" +from nltk.sentiment.sentiment_analyzer import SentimentAnalyzer +from nltk.sentiment.vader import SentimentIntensityAnalyzer diff --git a/nltk/sentiment/sentiment_analyzer.py b/nltk/sentiment/sentiment_analyzer.py new file mode 100644 index 0000000..8acaa97 --- /dev/null +++ b/nltk/sentiment/sentiment_analyzer.py @@ -0,0 +1,255 @@ +# +# Natural Language Toolkit: Sentiment Analyzer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +A SentimentAnalyzer is a tool to implement and facilitate Sentiment Analysis tasks +using NLTK features and classifiers, especially for teaching and demonstrative +purposes. +""" + +import sys +from collections import defaultdict + +from nltk.classify.util import accuracy as eval_accuracy +from nltk.classify.util import apply_features +from nltk.collocations import BigramCollocationFinder +from nltk.metrics import BigramAssocMeasures +from nltk.metrics import f_measure as eval_f_measure +from nltk.metrics import precision as eval_precision +from nltk.metrics import recall as eval_recall +from nltk.probability import FreqDist + + +class SentimentAnalyzer: + """ + A Sentiment Analysis tool based on machine learning approaches. + """ + + def __init__(self, classifier=None): + self.feat_extractors = defaultdict(list) + self.classifier = classifier + + def all_words(self, documents, labeled=None): + """ + Return all words/tokens from the documents (with duplicates). + + :param documents: a list of (words, label) tuples. + :param labeled: if `True`, assume that each document is represented by a + (words, label) tuple: (list(str), str). If `False`, each document is + considered as being a simple list of strings: list(str). + :rtype: list(str) + :return: A list of all words/tokens in `documents`. + """ + all_words = [] + if labeled is None: + labeled = documents and isinstance(documents[0], tuple) + if labeled: + for words, _sentiment in documents: + all_words.extend(words) + elif not labeled: + for words in documents: + all_words.extend(words) + return all_words + + def apply_features(self, documents, labeled=None): + """ + Apply all feature extractor functions to the documents. This is a wrapper + around `nltk.classify.util.apply_features`. + + If `labeled=False`, return featuresets as: + [feature_func(doc) for doc in documents] + If `labeled=True`, return featuresets as: + [(feature_func(tok), label) for (tok, label) in toks] + + :param documents: a list of documents. `If labeled=True`, the method expects + a list of (words, label) tuples. + :rtype: LazyMap + """ + return apply_features(self.extract_features, documents, labeled) + + def unigram_word_feats(self, words, top_n=None, min_freq=0): + """ + Return most common top_n word features. + + :param words: a list of words/tokens. + :param top_n: number of best words/tokens to use, sorted by frequency. + :rtype: list(str) + :return: A list of `top_n` words/tokens (with no duplicates) sorted by + frequency. + """ + # Stopwords are not removed + unigram_feats_freqs = FreqDist(word for word in words) + return [ + w + for w, f in unigram_feats_freqs.most_common(top_n) + if unigram_feats_freqs[w] > min_freq + ] + + def bigram_collocation_feats( + self, documents, top_n=None, min_freq=3, assoc_measure=BigramAssocMeasures.pmi + ): + """ + Return `top_n` bigram features (using `assoc_measure`). + Note that this method is based on bigram collocations measures, and not + on simple bigram frequency. + + :param documents: a list (or iterable) of tokens. + :param top_n: number of best words/tokens to use, sorted by association + measure. + :param assoc_measure: bigram association measure to use as score function. + :param min_freq: the minimum number of occurrencies of bigrams to take + into consideration. + + :return: `top_n` ngrams scored by the given association measure. + """ + finder = BigramCollocationFinder.from_documents(documents) + finder.apply_freq_filter(min_freq) + return finder.nbest(assoc_measure, top_n) + + def classify(self, instance): + """ + Classify a single instance applying the features that have already been + stored in the SentimentAnalyzer. + + :param instance: a list (or iterable) of tokens. + :return: the classification result given by applying the classifier. + """ + instance_feats = self.apply_features([instance], labeled=False) + return self.classifier.classify(instance_feats[0]) + + def add_feat_extractor(self, function, **kwargs): + """ + Add a new function to extract features from a document. This function will + be used in extract_features(). + Important: in this step our kwargs are only representing additional parameters, + and NOT the document we have to parse. The document will always be the first + parameter in the parameter list, and it will be added in the extract_features() + function. + + :param function: the extractor function to add to the list of feature extractors. + :param kwargs: additional parameters required by the `function` function. + """ + self.feat_extractors[function].append(kwargs) + + def extract_features(self, document): + """ + Apply extractor functions (and their parameters) to the present document. + We pass `document` as the first parameter of the extractor functions. + If we want to use the same extractor function multiple times, we have to + add it to the extractors with `add_feat_extractor` using multiple sets of + parameters (one for each call of the extractor function). + + :param document: the document that will be passed as argument to the + feature extractor functions. + :return: A dictionary of populated features extracted from the document. + :rtype: dict + """ + all_features = {} + for extractor in self.feat_extractors: + for param_set in self.feat_extractors[extractor]: + feats = extractor(document, **param_set) + all_features.update(feats) + return all_features + + def train(self, trainer, training_set, save_classifier=None, **kwargs): + """ + Train classifier on the training set, optionally saving the output in the + file specified by `save_classifier`. + Additional arguments depend on the specific trainer used. For example, + a MaxentClassifier can use `max_iter` parameter to specify the number + of iterations, while a NaiveBayesClassifier cannot. + + :param trainer: `train` method of a classifier. + E.g.: NaiveBayesClassifier.train + :param training_set: the training set to be passed as argument to the + classifier `train` method. + :param save_classifier: the filename of the file where the classifier + will be stored (optional). + :param kwargs: additional parameters that will be passed as arguments to + the classifier `train` function. + :return: A classifier instance trained on the training set. + :rtype: + """ + print("Training classifier") + self.classifier = trainer(training_set, **kwargs) + if save_classifier: + self.save_file(self.classifier, save_classifier) + + return self.classifier + + def save_file(self, content, filename): + """ + Store `content` in `filename`. Can be used to store a SentimentAnalyzer. + """ + print("Saving", filename, file=sys.stderr) + with open(filename, "wb") as storage_file: + import pickle + + # The protocol=2 parameter is for python2 compatibility + pickle.dump(content, storage_file, protocol=2) + + def evaluate( + self, + test_set, + classifier=None, + accuracy=True, + f_measure=True, + precision=True, + recall=True, + verbose=False, + ): + """ + Evaluate and print classifier performance on the test set. + + :param test_set: A list of (tokens, label) tuples to use as gold set. + :param classifier: a classifier instance (previously trained). + :param accuracy: if `True`, evaluate classifier accuracy. + :param f_measure: if `True`, evaluate classifier f_measure. + :param precision: if `True`, evaluate classifier precision. + :param recall: if `True`, evaluate classifier recall. + :return: evaluation results. + :rtype: dict(str): float + """ + if classifier is None: + classifier = self.classifier + print(f"Evaluating {type(classifier).__name__} results...") + metrics_results = {} + if accuracy: + accuracy_score = eval_accuracy(classifier, test_set) + metrics_results["Accuracy"] = accuracy_score + + gold_results = defaultdict(set) + test_results = defaultdict(set) + labels = set() + for i, (feats, label) in enumerate(test_set): + labels.add(label) + gold_results[label].add(i) + observed = classifier.classify(feats) + test_results[observed].add(i) + + for label in labels: + if precision: + precision_score = eval_precision( + gold_results[label], test_results[label] + ) + metrics_results[f"Precision [{label}]"] = precision_score + if recall: + recall_score = eval_recall(gold_results[label], test_results[label]) + metrics_results[f"Recall [{label}]"] = recall_score + if f_measure: + f_measure_score = eval_f_measure( + gold_results[label], test_results[label] + ) + metrics_results[f"F-measure [{label}]"] = f_measure_score + + # Print evaluation results (in alphabetical order) + if verbose: + for result in sorted(metrics_results): + print(f"{result}: {metrics_results[result]}") + + return metrics_results diff --git a/nltk/sentiment/util.py b/nltk/sentiment/util.py new file mode 100644 index 0000000..1be2201 --- /dev/null +++ b/nltk/sentiment/util.py @@ -0,0 +1,887 @@ +# +# Natural Language Toolkit: Sentiment Analyzer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Pierpaolo Pantone <24alsecondo@gmail.com> +# URL: +# For license information, see LICENSE.TXT + +""" +Utility methods for Sentiment Analysis. +""" + +import codecs +import csv +import json +import random +import re +import sys +import time +from copy import deepcopy + +import nltk +from nltk.corpus import CategorizedPlaintextCorpusReader +from nltk.data import load +from nltk.tokenize import PunktTokenizer +from nltk.tokenize.casual import EMOTICON_RE + +# //////////////////////////////////////////////////////////// +# { Regular expressions +# //////////////////////////////////////////////////////////// + +# Regular expression for negation by Christopher Potts +NEGATION = r""" + (?: + ^(?:never|no|nothing|nowhere|noone|none|not| + havent|hasnt|hadnt|cant|couldnt|shouldnt| + wont|wouldnt|dont|doesnt|didnt|isnt|arent|aint + )$ + ) + | + n't""" + +NEGATION_RE = re.compile(NEGATION, re.VERBOSE) + +CLAUSE_PUNCT = r"^[.:;!?]$" +CLAUSE_PUNCT_RE = re.compile(CLAUSE_PUNCT) + +# Happy and sad emoticons + +HAPPY = { + ":-)", + ":)", + ";)", + ":o)", + ":]", + ":3", + ":c)", + ":>", + "=]", + "8)", + "=)", + ":}", + ":^)", + ":-D", + ":D", + "8-D", + "8D", + "x-D", + "xD", + "X-D", + "XD", + "=-D", + "=D", + "=-3", + "=3", + ":-))", + ":'-)", + ":')", + ":*", + ":^*", + ">:P", + ":-P", + ":P", + "X-P", + "x-p", + "xp", + "XP", + ":-p", + ":p", + "=p", + ":-b", + ":b", + ">:)", + ">;)", + ">:-)", + "<3", +} + +SAD = { + ":L", + ":-/", + ">:/", + ":S", + ">:[", + ":@", + ":-(", + ":[", + ":-||", + "=L", + ":<", + ":-[", + ":-<", + "=\\", + "=/", + ">:(", + ":(", + ">.<", + ":'-(", + ":'(", + ":\\", + ":-c", + ":c", + ":{", + ">:\\", + ";(", +} + + +def timer(method): + """ + A timer decorator to measure execution performance of methods. + """ + + def timed(*args, **kw): + start = time.time() + result = method(*args, **kw) + end = time.time() + tot_time = end - start + hours = tot_time // 3600 + mins = tot_time // 60 % 60 + # in Python 2.x round() will return a float, so we convert it to int + secs = int(round(tot_time % 60)) + if hours == 0 and mins == 0 and secs < 10: + print(f"[TIMER] {method.__name__}(): {method.__name__:.3f} seconds") + else: + print(f"[TIMER] {method.__name__}(): {hours}h {mins}m {secs}s") + return result + + return timed + + +# //////////////////////////////////////////////////////////// +# { Feature extractor functions +# //////////////////////////////////////////////////////////// +""" +Feature extractor functions are declared outside the SentimentAnalyzer class. +Users should have the possibility to create their own feature extractors +without modifying SentimentAnalyzer. +""" + + +def extract_unigram_feats(document, unigrams, handle_negation=False): + """ + Populate a dictionary of unigram features, reflecting the presence/absence in + the document of each of the tokens in `unigrams`. + + :param document: a list of words/tokens. + :param unigrams: a list of words/tokens whose presence/absence has to be + checked in `document`. + :param handle_negation: if `handle_negation == True` apply `mark_negation` + method to `document` before checking for unigram presence/absence. + :return: a dictionary of unigram features {unigram : boolean}. + + >>> words = ['ice', 'police', 'riot'] + >>> document = 'ice is melting due to global warming'.split() + >>> sorted(extract_unigram_feats(document, words).items()) + [('contains(ice)', True), ('contains(police)', False), ('contains(riot)', False)] + """ + features = {} + if handle_negation: + document = mark_negation(document) + for word in unigrams: + features[f"contains({word})"] = word in set(document) + return features + + +def extract_bigram_feats(document, bigrams): + """ + Populate a dictionary of bigram features, reflecting the presence/absence in + the document of each of the tokens in `bigrams`. This extractor function only + considers contiguous bigrams obtained by `nltk.bigrams`. + + :param document: a list of words/tokens. + :param unigrams: a list of bigrams whose presence/absence has to be + checked in `document`. + :return: a dictionary of bigram features {bigram : boolean}. + + >>> bigrams = [('global', 'warming'), ('police', 'prevented'), ('love', 'you')] + >>> document = 'ice is melting due to global warming'.split() + >>> sorted(extract_bigram_feats(document, bigrams).items()) # doctest: +NORMALIZE_WHITESPACE + [('contains(global - warming)', True), ('contains(love - you)', False), + ('contains(police - prevented)', False)] + """ + features = {} + for bigr in bigrams: + features[f"contains({bigr[0]} - {bigr[1]})"] = bigr in nltk.bigrams(document) + return features + + +# //////////////////////////////////////////////////////////// +# { Helper Functions +# //////////////////////////////////////////////////////////// + + +def mark_negation(document, double_neg_flip=False, shallow=False): + """ + Append _NEG suffix to words that appear in the scope between a negation + and a punctuation mark. + + :param document: a list of words/tokens, or a tuple (words, label). + :param shallow: if True, the method will modify the original document in place. + :param double_neg_flip: if True, double negation is considered affirmation + (we activate/deactivate negation scope every time we find a negation). + :return: if `shallow == True` the method will modify the original document + and return it. If `shallow == False` the method will return a modified + document, leaving the original unmodified. + + >>> sent = "I didn't like this movie . It was bad .".split() + >>> mark_negation(sent) + ['I', "didn't", 'like_NEG', 'this_NEG', 'movie_NEG', '.', 'It', 'was', 'bad', '.'] + """ + if not shallow: + document = deepcopy(document) + # check if the document is labeled. If so, do not consider the label. + labeled = document and isinstance(document[0], (tuple, list)) + if labeled: + doc = document[0] + else: + doc = document + neg_scope = False + for i, word in enumerate(doc): + if NEGATION_RE.search(word): + if not neg_scope or (neg_scope and double_neg_flip): + neg_scope = not neg_scope + continue + else: + doc[i] += "_NEG" + elif neg_scope and CLAUSE_PUNCT_RE.search(word): + neg_scope = not neg_scope + elif neg_scope and not CLAUSE_PUNCT_RE.search(word): + doc[i] += "_NEG" + + return document + + +def output_markdown(filename, **kwargs): + """ + Write the output of an analysis to a file. + """ + with codecs.open(filename, "at") as outfile: + text = "\n*** \n\n" + text += "{} \n\n".format(time.strftime("%d/%m/%Y, %H:%M")) + for k in sorted(kwargs): + if isinstance(kwargs[k], dict): + dictionary = kwargs[k] + text += f" - **{k}:**\n" + for entry in sorted(dictionary): + text += f" - {entry}: {dictionary[entry]} \n" + elif isinstance(kwargs[k], list): + text += f" - **{k}:**\n" + for entry in kwargs[k]: + text += f" - {entry}\n" + else: + text += f" - **{k}:** {kwargs[k]} \n" + outfile.write(text) + + +def split_train_test(all_instances, n=None): + """ + Randomly split `n` instances of the dataset into train and test sets. + + :param all_instances: a list of instances (e.g. documents) that will be split. + :param n: the number of instances to consider (in case we want to use only a + subset). + :return: two lists of instances. Train set is 8/10 of the total and test set + is 2/10 of the total. + """ + random.seed(12345) + random.shuffle(all_instances) + if not n or n > len(all_instances): + n = len(all_instances) + train_set = all_instances[: int(0.8 * n)] + test_set = all_instances[int(0.8 * n) : n] + + return train_set, test_set + + +def _show_plot(x_values, y_values, x_labels=None, y_labels=None): + try: + import matplotlib.pyplot as plt + except ImportError as e: + raise ImportError( + "The plot function requires matplotlib to be installed." + "See https://matplotlib.org/" + ) from e + + plt.locator_params(axis="y", nbins=3) + axes = plt.axes() + axes.yaxis.grid() + plt.plot(x_values, y_values, "ro", color="red") + plt.ylim(ymin=-1.2, ymax=1.2) + plt.tight_layout(pad=5) + if x_labels: + plt.xticks(x_values, x_labels, rotation="vertical") + if y_labels: + plt.yticks([-1, 0, 1], y_labels, rotation="horizontal") + # Pad margins so that markers are not clipped by the axes + plt.margins(0.2) + plt.show() + + +# //////////////////////////////////////////////////////////// +# { Parsing and conversion functions +# //////////////////////////////////////////////////////////// + + +def json2csv_preprocess( + json_file, + outfile, + fields, + encoding="utf8", + errors="replace", + gzip_compress=False, + skip_retweets=True, + skip_tongue_tweets=True, + skip_ambiguous_tweets=True, + strip_off_emoticons=True, + remove_duplicates=True, + limit=None, +): + """ + Convert json file to csv file, preprocessing each row to obtain a suitable + dataset for tweets Semantic Analysis. + + :param json_file: the original json file containing tweets. + :param outfile: the output csv filename. + :param fields: a list of fields that will be extracted from the json file and + kept in the output csv file. + :param encoding: the encoding of the files. + :param errors: the error handling strategy for the output writer. + :param gzip_compress: if True, create a compressed GZIP file. + + :param skip_retweets: if True, remove retweets. + :param skip_tongue_tweets: if True, remove tweets containing ":P" and ":-P" + emoticons. + :param skip_ambiguous_tweets: if True, remove tweets containing both happy + and sad emoticons. + :param strip_off_emoticons: if True, strip off emoticons from all tweets. + :param remove_duplicates: if True, remove tweets appearing more than once. + :param limit: an integer to set the number of tweets to convert. After the + limit is reached the conversion will stop. It can be useful to create + subsets of the original tweets json data. + """ + with codecs.open(json_file, encoding=encoding) as fp: + (writer, outf) = _outf_writer(outfile, encoding, errors, gzip_compress) + # write the list of fields as header + writer.writerow(fields) + + if remove_duplicates: + tweets_cache = [] + i = 0 + for line in fp: + tweet = json.loads(line) + row = extract_fields(tweet, fields) + try: + text = row[fields.index("text")] + # Remove retweets + if skip_retweets: + if re.search(r"\bRT\b", text): + continue + # Remove tweets containing ":P" and ":-P" emoticons + if skip_tongue_tweets: + if re.search(r"\:\-?P\b", text): + continue + # Remove tweets containing both happy and sad emoticons + if skip_ambiguous_tweets: + all_emoticons = EMOTICON_RE.findall(text) + if all_emoticons: + if (set(all_emoticons) & HAPPY) and (set(all_emoticons) & SAD): + continue + # Strip off emoticons from all tweets + if strip_off_emoticons: + row[fields.index("text")] = re.sub( + r"(?!\n)\s+", " ", EMOTICON_RE.sub("", text) + ) + # Remove duplicate tweets + if remove_duplicates: + if row[fields.index("text")] in tweets_cache: + continue + else: + tweets_cache.append(row[fields.index("text")]) + except ValueError: + pass + writer.writerow(row) + i += 1 + if limit and i >= limit: + break + outf.close() + + +def parse_tweets_set( + filename, label, word_tokenizer=None, sent_tokenizer=None, skip_header=True +): + """ + Parse csv file containing tweets and output data a list of (text, label) tuples. + + :param filename: the input csv filename. + :param label: the label to be appended to each tweet contained in the csv file. + :param word_tokenizer: the tokenizer instance that will be used to tokenize + each sentence into tokens (e.g. WordPunctTokenizer() or BlanklineTokenizer()). + If no word_tokenizer is specified, tweets will not be tokenized. + :param sent_tokenizer: the tokenizer that will be used to split each tweet into + sentences. + :param skip_header: if True, skip the first line of the csv file (which usually + contains headers). + + :return: a list of (text, label) tuples. + """ + tweets = [] + if not sent_tokenizer: + sent_tokenizer = PunktTokenizer() + + with codecs.open(filename, "rt") as csvfile: + reader = csv.reader(csvfile) + if skip_header: + next(reader, None) # skip the header + i = 0 + for tweet_id, text in reader: + # text = text[1] + i += 1 + sys.stdout.write(f"Loaded {i} tweets\r") + # Apply sentence and word tokenizer to text + if word_tokenizer: + tweet = [ + w + for sent in sent_tokenizer.tokenize(text) + for w in word_tokenizer.tokenize(sent) + ] + else: + tweet = text + tweets.append((tweet, label)) + + print(f"Loaded {i} tweets") + return tweets + + +# //////////////////////////////////////////////////////////// +# { Demos +# //////////////////////////////////////////////////////////// + + +def demo_tweets(trainer, n_instances=None, output=None): + """ + Train and test Naive Bayes classifier on 10000 tweets, tokenized using + TweetTokenizer. + Features are composed of: + + - 1000 most frequent unigrams + - 100 top bigrams (using BigramAssocMeasures.pmi) + + :param trainer: `train` method of a classifier. + :param n_instances: the number of total tweets that have to be used for + training and testing. Tweets will be equally split between positive and + negative. + :param output: the output file where results have to be reported. + """ + from nltk.corpus import stopwords, twitter_samples + from nltk.sentiment import SentimentAnalyzer + from nltk.tokenize import TweetTokenizer + + # Different customizations for the TweetTokenizer + tokenizer = TweetTokenizer(preserve_case=False) + # tokenizer = TweetTokenizer(preserve_case=True, strip_handles=True) + # tokenizer = TweetTokenizer(reduce_len=True, strip_handles=True) + + if n_instances is not None: + n_instances = int(n_instances / 2) + + fields = ["id", "text"] + positive_json = twitter_samples.abspath("positive_tweets.json") + positive_csv = "positive_tweets.csv" + json2csv_preprocess(positive_json, positive_csv, fields, limit=n_instances) + + negative_json = twitter_samples.abspath("negative_tweets.json") + negative_csv = "negative_tweets.csv" + json2csv_preprocess(negative_json, negative_csv, fields, limit=n_instances) + + neg_docs = parse_tweets_set(negative_csv, label="neg", word_tokenizer=tokenizer) + pos_docs = parse_tweets_set(positive_csv, label="pos", word_tokenizer=tokenizer) + + # We separately split subjective and objective instances to keep a balanced + # uniform class distribution in both train and test sets. + train_pos_docs, test_pos_docs = split_train_test(pos_docs) + train_neg_docs, test_neg_docs = split_train_test(neg_docs) + + training_tweets = train_pos_docs + train_neg_docs + testing_tweets = test_pos_docs + test_neg_docs + + sentim_analyzer = SentimentAnalyzer() + # stopwords = stopwords.words('english') + # all_words = [word for word in sentim_analyzer.all_words(training_tweets) if word.lower() not in stopwords] + all_words = [word for word in sentim_analyzer.all_words(training_tweets)] + + # Add simple unigram word features + unigram_feats = sentim_analyzer.unigram_word_feats(all_words, top_n=1000) + sentim_analyzer.add_feat_extractor(extract_unigram_feats, unigrams=unigram_feats) + + # Add bigram collocation features + bigram_collocs_feats = sentim_analyzer.bigram_collocation_feats( + [tweet[0] for tweet in training_tweets], top_n=100, min_freq=12 + ) + sentim_analyzer.add_feat_extractor( + extract_bigram_feats, bigrams=bigram_collocs_feats + ) + + training_set = sentim_analyzer.apply_features(training_tweets) + test_set = sentim_analyzer.apply_features(testing_tweets) + + classifier = sentim_analyzer.train(trainer, training_set) + # classifier = sentim_analyzer.train(trainer, training_set, max_iter=4) + try: + classifier.show_most_informative_features() + except AttributeError: + print( + "Your classifier does not provide a show_most_informative_features() method." + ) + results = sentim_analyzer.evaluate(test_set) + + if output: + extr = [f.__name__ for f in sentim_analyzer.feat_extractors] + output_markdown( + output, + Dataset="labeled_tweets", + Classifier=type(classifier).__name__, + Tokenizer=tokenizer.__class__.__name__, + Feats=extr, + Results=results, + Instances=n_instances, + ) + + +def demo_movie_reviews(trainer, n_instances=None, output=None): + """ + Train classifier on all instances of the Movie Reviews dataset. + The corpus has been preprocessed using the default sentence tokenizer and + WordPunctTokenizer. + Features are composed of: + + - most frequent unigrams + + :param trainer: `train` method of a classifier. + :param n_instances: the number of total reviews that have to be used for + training and testing. Reviews will be equally split between positive and + negative. + :param output: the output file where results have to be reported. + """ + from nltk.corpus import movie_reviews + from nltk.sentiment import SentimentAnalyzer + + if n_instances is not None: + n_instances = int(n_instances / 2) + + pos_docs = [ + (list(movie_reviews.words(pos_id)), "pos") + for pos_id in movie_reviews.fileids("pos")[:n_instances] + ] + neg_docs = [ + (list(movie_reviews.words(neg_id)), "neg") + for neg_id in movie_reviews.fileids("neg")[:n_instances] + ] + # We separately split positive and negative instances to keep a balanced + # uniform class distribution in both train and test sets. + train_pos_docs, test_pos_docs = split_train_test(pos_docs) + train_neg_docs, test_neg_docs = split_train_test(neg_docs) + + training_docs = train_pos_docs + train_neg_docs + testing_docs = test_pos_docs + test_neg_docs + + sentim_analyzer = SentimentAnalyzer() + all_words = sentim_analyzer.all_words(training_docs) + + # Add simple unigram word features + unigram_feats = sentim_analyzer.unigram_word_feats(all_words, min_freq=4) + sentim_analyzer.add_feat_extractor(extract_unigram_feats, unigrams=unigram_feats) + # Apply features to obtain a feature-value representation of our datasets + training_set = sentim_analyzer.apply_features(training_docs) + test_set = sentim_analyzer.apply_features(testing_docs) + + classifier = sentim_analyzer.train(trainer, training_set) + try: + classifier.show_most_informative_features() + except AttributeError: + print( + "Your classifier does not provide a show_most_informative_features() method." + ) + results = sentim_analyzer.evaluate(test_set) + + if output: + extr = [f.__name__ for f in sentim_analyzer.feat_extractors] + output_markdown( + output, + Dataset="Movie_reviews", + Classifier=type(classifier).__name__, + Tokenizer="WordPunctTokenizer", + Feats=extr, + Results=results, + Instances=n_instances, + ) + + +def demo_subjectivity(trainer, save_analyzer=False, n_instances=None, output=None): + """ + Train and test a classifier on instances of the Subjective Dataset by Pang and + Lee. The dataset is made of 5000 subjective and 5000 objective sentences. + All tokens (words and punctuation marks) are separated by a whitespace, so + we use the basic WhitespaceTokenizer to parse the data. + + :param trainer: `train` method of a classifier. + :param save_analyzer: if `True`, store the SentimentAnalyzer in a pickle file. + :param n_instances: the number of total sentences that have to be used for + training and testing. Sentences will be equally split between positive + and negative. + :param output: the output file where results have to be reported. + """ + from nltk.corpus import subjectivity + from nltk.sentiment import SentimentAnalyzer + + if n_instances is not None: + n_instances = int(n_instances / 2) + + subj_docs = [ + (sent, "subj") for sent in subjectivity.sents(categories="subj")[:n_instances] + ] + obj_docs = [ + (sent, "obj") for sent in subjectivity.sents(categories="obj")[:n_instances] + ] + + # We separately split subjective and objective instances to keep a balanced + # uniform class distribution in both train and test sets. + train_subj_docs, test_subj_docs = split_train_test(subj_docs) + train_obj_docs, test_obj_docs = split_train_test(obj_docs) + + training_docs = train_subj_docs + train_obj_docs + testing_docs = test_subj_docs + test_obj_docs + + sentim_analyzer = SentimentAnalyzer() + all_words_neg = sentim_analyzer.all_words( + [mark_negation(doc) for doc in training_docs] + ) + + # Add simple unigram word features handling negation + unigram_feats = sentim_analyzer.unigram_word_feats(all_words_neg, min_freq=4) + sentim_analyzer.add_feat_extractor(extract_unigram_feats, unigrams=unigram_feats) + + # Apply features to obtain a feature-value representation of our datasets + training_set = sentim_analyzer.apply_features(training_docs) + test_set = sentim_analyzer.apply_features(testing_docs) + + classifier = sentim_analyzer.train(trainer, training_set) + try: + classifier.show_most_informative_features() + except AttributeError: + print( + "Your classifier does not provide a show_most_informative_features() method." + ) + results = sentim_analyzer.evaluate(test_set) + + if save_analyzer: + sentim_analyzer.save_file(sentim_analyzer, "sa_subjectivity.pickle") + + if output: + extr = [f.__name__ for f in sentim_analyzer.feat_extractors] + output_markdown( + output, + Dataset="subjectivity", + Classifier=type(classifier).__name__, + Tokenizer="WhitespaceTokenizer", + Feats=extr, + Instances=n_instances, + Results=results, + ) + + return sentim_analyzer + + +def demo_sent_subjectivity(text): + """ + Classify a single sentence as subjective or objective using a stored + SentimentAnalyzer. + + :param text: a sentence whose subjectivity has to be classified. + """ + from nltk.classify import NaiveBayesClassifier + from nltk.tokenize import regexp + + word_tokenizer = regexp.WhitespaceTokenizer() + try: + sentim_analyzer = load("sa_subjectivity.pickle") + except LookupError: + print("Cannot find the sentiment analyzer you want to load.") + print("Training a new one using NaiveBayesClassifier.") + sentim_analyzer = demo_subjectivity(NaiveBayesClassifier.train, True) + + # Tokenize and convert to lower case + tokenized_text = [word.lower() for word in word_tokenizer.tokenize(text)] + print(sentim_analyzer.classify(tokenized_text)) + + +def demo_liu_hu_lexicon(sentence, plot=False): + """ + Basic example of sentiment classification using Liu and Hu opinion lexicon. + This function simply counts the number of positive, negative and neutral words + in the sentence and classifies it depending on which polarity is more represented. + Words that do not appear in the lexicon are considered as neutral. + + :param sentence: a sentence whose polarity has to be classified. + :param plot: if True, plot a visual representation of the sentence polarity. + """ + from nltk.corpus import opinion_lexicon + from nltk.tokenize import treebank + + tokenizer = treebank.TreebankWordTokenizer() + pos_words = 0 + neg_words = 0 + tokenized_sent = [word.lower() for word in tokenizer.tokenize(sentence)] + + x = list(range(len(tokenized_sent))) # x axis for the plot + y = [] + + for word in tokenized_sent: + if word in opinion_lexicon.positive(): + pos_words += 1 + y.append(1) # positive + elif word in opinion_lexicon.negative(): + neg_words += 1 + y.append(-1) # negative + else: + y.append(0) # neutral + + if pos_words > neg_words: + print("Positive") + elif pos_words < neg_words: + print("Negative") + elif pos_words == neg_words: + print("Neutral") + + if plot: + _show_plot( + x, y, x_labels=tokenized_sent, y_labels=["Negative", "Neutral", "Positive"] + ) + + +def demo_vader_instance(text): + """ + Output polarity scores for a text using Vader approach. + + :param text: a text whose polarity has to be evaluated. + """ + from nltk.sentiment import SentimentIntensityAnalyzer + + vader_analyzer = SentimentIntensityAnalyzer() + print(vader_analyzer.polarity_scores(text)) + + +def demo_vader_tweets(n_instances=None, output=None): + """ + Classify 10000 positive and negative tweets using Vader approach. + + :param n_instances: the number of total tweets that have to be classified. + :param output: the output file where results have to be reported. + """ + from collections import defaultdict + + from nltk.corpus import twitter_samples + from nltk.metrics import accuracy as eval_accuracy + from nltk.metrics import f_measure as eval_f_measure + from nltk.metrics import precision as eval_precision + from nltk.metrics import recall as eval_recall + from nltk.sentiment import SentimentIntensityAnalyzer + + if n_instances is not None: + n_instances = int(n_instances / 2) + + fields = ["id", "text"] + positive_json = twitter_samples.abspath("positive_tweets.json") + positive_csv = "positive_tweets.csv" + json2csv_preprocess( + positive_json, + positive_csv, + fields, + strip_off_emoticons=False, + limit=n_instances, + ) + + negative_json = twitter_samples.abspath("negative_tweets.json") + negative_csv = "negative_tweets.csv" + json2csv_preprocess( + negative_json, + negative_csv, + fields, + strip_off_emoticons=False, + limit=n_instances, + ) + + pos_docs = parse_tweets_set(positive_csv, label="pos") + neg_docs = parse_tweets_set(negative_csv, label="neg") + + # We separately split subjective and objective instances to keep a balanced + # uniform class distribution in both train and test sets. + train_pos_docs, test_pos_docs = split_train_test(pos_docs) + train_neg_docs, test_neg_docs = split_train_test(neg_docs) + + training_tweets = train_pos_docs + train_neg_docs + testing_tweets = test_pos_docs + test_neg_docs + + vader_analyzer = SentimentIntensityAnalyzer() + + gold_results = defaultdict(set) + test_results = defaultdict(set) + acc_gold_results = [] + acc_test_results = [] + labels = set() + num = 0 + for i, (text, label) in enumerate(testing_tweets): + labels.add(label) + gold_results[label].add(i) + acc_gold_results.append(label) + score = vader_analyzer.polarity_scores(text)["compound"] + if score > 0: + observed = "pos" + else: + observed = "neg" + num += 1 + acc_test_results.append(observed) + test_results[observed].add(i) + metrics_results = {} + for label in labels: + accuracy_score = eval_accuracy(acc_gold_results, acc_test_results) + metrics_results["Accuracy"] = accuracy_score + precision_score = eval_precision(gold_results[label], test_results[label]) + metrics_results[f"Precision [{label}]"] = precision_score + recall_score = eval_recall(gold_results[label], test_results[label]) + metrics_results[f"Recall [{label}]"] = recall_score + f_measure_score = eval_f_measure(gold_results[label], test_results[label]) + metrics_results[f"F-measure [{label}]"] = f_measure_score + + for result in sorted(metrics_results): + print(f"{result}: {metrics_results[result]}") + + if output: + output_markdown( + output, + Approach="Vader", + Dataset="labeled_tweets", + Instances=n_instances, + Results=metrics_results, + ) + + +if __name__ == "__main__": + from sklearn.svm import LinearSVC + + from nltk.classify import MaxentClassifier, NaiveBayesClassifier + from nltk.classify.scikitlearn import SklearnClassifier + from nltk.twitter.common import _outf_writer, extract_fields + + naive_bayes = NaiveBayesClassifier.train + svm = SklearnClassifier(LinearSVC()).train + maxent = MaxentClassifier.train + + demo_tweets(naive_bayes) + # demo_movie_reviews(svm) + # demo_subjectivity(svm) + # demo_sent_subjectivity("she's an artist , but hasn't picked up a brush in a year . ") + # demo_liu_hu_lexicon("This movie was actually neither that funny, nor super witty.", plot=True) + # demo_vader_instance("This movie was actually neither that funny, nor super witty.") + # demo_vader_tweets() diff --git a/nltk/sentiment/vader.py b/nltk/sentiment/vader.py new file mode 100644 index 0000000..6fa0a93 --- /dev/null +++ b/nltk/sentiment/vader.py @@ -0,0 +1,643 @@ +# Natural Language Toolkit: vader +# +# Copyright (C) 2001-2026 NLTK Project +# Author: C.J. Hutto +# Ewan Klein (modifications) +# Pierpaolo Pantone <24alsecondo@gmail.com> (modifications) +# George Berry (modifications) +# Malavika Suresh (modifications) +# URL: +# For license information, see LICENSE.TXT +# +# Modifications to the original VADER code have been made in order to +# integrate it into NLTK. These have involved changes to +# ensure Python 3 compatibility, and refactoring to achieve greater modularity. + +""" +If you use the VADER sentiment analysis tools, please cite: + +Hutto, C.J. & Gilbert, E.E. (2014). VADER: A Parsimonious Rule-based Model for +Sentiment Analysis of Social Media Text. Eighth International Conference on +Weblogs and Social Media (ICWSM-14). Ann Arbor, MI, June 2014. +""" + +import math +import re +import string +from itertools import product + +import nltk.data +from nltk.util import pairwise + + +class VaderConstants: + """ + A class to keep the Vader lists and constants. + """ + + ##Constants## + # (empirically derived mean sentiment intensity rating increase for booster words) + B_INCR = 0.293 + B_DECR = -0.293 + + # (empirically derived mean sentiment intensity rating increase for using + # ALLCAPs to emphasize a word) + C_INCR = 0.733 + + N_SCALAR = -0.74 + + NEGATE = { + "aint", + "arent", + "cannot", + "cant", + "couldnt", + "darent", + "didnt", + "doesnt", + "ain't", + "aren't", + "can't", + "couldn't", + "daren't", + "didn't", + "doesn't", + "dont", + "hadnt", + "hasnt", + "havent", + "isnt", + "mightnt", + "mustnt", + "neither", + "don't", + "hadn't", + "hasn't", + "haven't", + "isn't", + "mightn't", + "mustn't", + "neednt", + "needn't", + "never", + "none", + "nope", + "nor", + "not", + "nothing", + "nowhere", + "oughtnt", + "shant", + "shouldnt", + "uhuh", + "wasnt", + "werent", + "oughtn't", + "shan't", + "shouldn't", + "uh-uh", + "wasn't", + "weren't", + "without", + "wont", + "wouldnt", + "won't", + "wouldn't", + "rarely", + "seldom", + "despite", + } + + # booster/dampener 'intensifiers' or 'degree adverbs' + # https://en.wiktionary.org/wiki/Category:English_degree_adverbs + + BOOSTER_DICT = { + "absolutely": B_INCR, + "amazingly": B_INCR, + "awfully": B_INCR, + "completely": B_INCR, + "considerably": B_INCR, + "decidedly": B_INCR, + "deeply": B_INCR, + "effing": B_INCR, + "enormously": B_INCR, + "entirely": B_INCR, + "especially": B_INCR, + "exceptionally": B_INCR, + "extremely": B_INCR, + "fabulously": B_INCR, + "flipping": B_INCR, + "flippin": B_INCR, + "fricking": B_INCR, + "frickin": B_INCR, + "frigging": B_INCR, + "friggin": B_INCR, + "fully": B_INCR, + "fucking": B_INCR, + "greatly": B_INCR, + "hella": B_INCR, + "highly": B_INCR, + "hugely": B_INCR, + "incredibly": B_INCR, + "intensely": B_INCR, + "majorly": B_INCR, + "more": B_INCR, + "most": B_INCR, + "particularly": B_INCR, + "purely": B_INCR, + "quite": B_INCR, + "really": B_INCR, + "remarkably": B_INCR, + "so": B_INCR, + "substantially": B_INCR, + "thoroughly": B_INCR, + "totally": B_INCR, + "tremendously": B_INCR, + "uber": B_INCR, + "unbelievably": B_INCR, + "unusually": B_INCR, + "utterly": B_INCR, + "very": B_INCR, + "almost": B_DECR, + "barely": B_DECR, + "hardly": B_DECR, + "just enough": B_DECR, + "kind of": B_DECR, + "kinda": B_DECR, + "kindof": B_DECR, + "kind-of": B_DECR, + "less": B_DECR, + "little": B_DECR, + "marginally": B_DECR, + "occasionally": B_DECR, + "partly": B_DECR, + "scarcely": B_DECR, + "slightly": B_DECR, + "somewhat": B_DECR, + "sort of": B_DECR, + "sorta": B_DECR, + "sortof": B_DECR, + "sort-of": B_DECR, + } + + # check for special case idioms using a sentiment-laden keyword known to SAGE + SPECIAL_CASE_IDIOMS = { + "the shit": 3, + "the bomb": 3, + "bad ass": 1.5, + "yeah right": -2, + "cut the mustard": 2, + "kiss of death": -1.5, + "hand to mouth": -2, + } + + # for removing punctuation + REGEX_REMOVE_PUNCTUATION = re.compile(f"[{re.escape(string.punctuation)}]") + + PUNC_LIST = [ + ".", + "!", + "?", + ",", + ";", + ":", + "-", + "'", + '"', + "!!", + "!!!", + "??", + "???", + "?!?", + "!?!", + "?!?!", + "!?!?", + ] + + def __init__(self): + pass + + def negated(self, input_words, include_nt=True): + """ + Determine if input contains negation words + """ + neg_words = self.NEGATE + if any(word.lower() in neg_words for word in input_words): + return True + if include_nt: + if any("n't" in word.lower() for word in input_words): + return True + for first, second in pairwise(input_words): + if second.lower() == "least" and first.lower() != "at": + return True + return False + + def normalize(self, score, alpha=15): + """ + Normalize the score to be between -1 and 1 using an alpha that + approximates the max expected value + """ + norm_score = score / math.sqrt((score * score) + alpha) + return norm_score + + def scalar_inc_dec(self, word, valence, is_cap_diff): + """ + Check if the preceding words increase, decrease, or negate/nullify the + valence + """ + scalar = 0.0 + word_lower = word.lower() + if word_lower in self.BOOSTER_DICT: + scalar = self.BOOSTER_DICT[word_lower] + if valence < 0: + scalar *= -1 + # check if booster/dampener word is in ALLCAPS (while others aren't) + if word.isupper() and is_cap_diff: + if valence > 0: + scalar += self.C_INCR + else: + scalar -= self.C_INCR + return scalar + + +class SentiText: + """ + Identify sentiment-relevant string-level properties of input text. + """ + + def __init__(self, text, punc_list, regex_remove_punctuation): + if not isinstance(text, str): + text = str(text.encode("utf-8")) + self.text = text + self.PUNC_LIST = punc_list + self.REGEX_REMOVE_PUNCTUATION = regex_remove_punctuation + self.words_and_emoticons = self._words_and_emoticons() + # doesn't separate words from + # adjacent punctuation (keeps emoticons & contractions) + self.is_cap_diff = self.allcap_differential(self.words_and_emoticons) + + def _words_plus_punc(self): + """ + Returns mapping of form: + { + 'cat,': 'cat', + ',cat': 'cat', + } + """ + no_punc_text = self.REGEX_REMOVE_PUNCTUATION.sub("", self.text) + # removes punctuation (but loses emoticons & contractions) + words_only = no_punc_text.split() + # remove singletons + words_only = {w for w in words_only if len(w) > 1} + # the product gives ('cat', ',') and (',', 'cat') + punc_before = {"".join(p): p[1] for p in product(self.PUNC_LIST, words_only)} + punc_after = {"".join(p): p[0] for p in product(words_only, self.PUNC_LIST)} + words_punc_dict = punc_before + words_punc_dict.update(punc_after) + return words_punc_dict + + def _words_and_emoticons(self): + """ + Removes leading and trailing puncutation + Leaves contractions and most emoticons + Does not preserve punc-plus-letter emoticons (e.g. :D) + """ + wes = self.text.split() + words_punc_dict = self._words_plus_punc() + wes = [we for we in wes if len(we) > 1] + for i, we in enumerate(wes): + if we in words_punc_dict: + wes[i] = words_punc_dict[we] + return wes + + def allcap_differential(self, words): + """ + Check whether just some words in the input are ALL CAPS + + :param list words: The words to inspect + :returns: `True` if some but not all items in `words` are ALL CAPS + """ + is_different = False + allcap_words = 0 + for word in words: + if word.isupper(): + allcap_words += 1 + cap_differential = len(words) - allcap_words + if 0 < cap_differential < len(words): + is_different = True + return is_different + + +class SentimentIntensityAnalyzer: + """ + Give a sentiment intensity score to sentences. + """ + + def __init__( + self, + lexicon_file="sentiment/vader_lexicon.zip/vader_lexicon/vader_lexicon.txt", + ): + self.lexicon_file = nltk.data.load(lexicon_file) + self.lexicon = self.make_lex_dict() + self.constants = VaderConstants() + + def make_lex_dict(self): + """ + Convert lexicon file to a dictionary + """ + lex_dict = {} + for line in self.lexicon_file.split("\n"): + (word, measure) = line.strip().split("\t")[0:2] + lex_dict[word] = float(measure) + return lex_dict + + def polarity_scores(self, text): + """ + Return a float for sentiment strength based on the input text. + Positive values are positive valence, negative value are negative + valence. + + :note: Hashtags are not taken into consideration (e.g. #BAD is neutral). If you + are interested in processing the text in the hashtags too, then we recommend + preprocessing your data to remove the #, after which the hashtag text may be + matched as if it was a normal word in the sentence. + """ + # text, words_and_emoticons, is_cap_diff = self.preprocess(text) + sentitext = SentiText( + text, self.constants.PUNC_LIST, self.constants.REGEX_REMOVE_PUNCTUATION + ) + sentiments = [] + words_and_emoticons = sentitext.words_and_emoticons + # Precompute the first index of each token once, in O(n). The loop below + # previously called ``words_and_emoticons.index(item)`` on every + # iteration -- an O(n) scan per token, i.e. O(n^2) overall on text with + # many distinct tokens, a remote CPU DoS (CWE-407). ``list.index`` + # returns the first matching position, so this mapping yields the exact + # same indices in linear time. + first_index = {} + for idx, token in enumerate(words_and_emoticons): + if token not in first_index: + first_index[token] = idx + for item in words_and_emoticons: + valence = 0 + i = first_index[item] + if ( + i < len(words_and_emoticons) - 1 + and item.lower() == "kind" + and words_and_emoticons[i + 1].lower() == "of" + ) or item.lower() in self.constants.BOOSTER_DICT: + sentiments.append(valence) + continue + + sentiments = self.sentiment_valence(valence, sentitext, item, i, sentiments) + + sentiments = self._but_check(words_and_emoticons, sentiments) + + return self.score_valence(sentiments, text) + + def sentiment_valence(self, valence, sentitext, item, i, sentiments): + is_cap_diff = sentitext.is_cap_diff + words_and_emoticons = sentitext.words_and_emoticons + item_lowercase = item.lower() + if item_lowercase in self.lexicon: + # get the sentiment valence + valence = self.lexicon[item_lowercase] + + # check if sentiment laden word is in ALL CAPS (while others aren't) + if item.isupper() and is_cap_diff: + if valence > 0: + valence += self.constants.C_INCR + else: + valence -= self.constants.C_INCR + + for start_i in range(0, 3): + if ( + i > start_i + and words_and_emoticons[i - (start_i + 1)].lower() + not in self.lexicon + ): + # dampen the scalar modifier of preceding words and emoticons + # (excluding the ones that immediately preceed the item) based + # on their distance from the current item. + s = self.constants.scalar_inc_dec( + words_and_emoticons[i - (start_i + 1)], valence, is_cap_diff + ) + if start_i == 1 and s != 0: + s = s * 0.95 + if start_i == 2 and s != 0: + s = s * 0.9 + valence = valence + s + valence = self._never_check( + valence, words_and_emoticons, start_i, i + ) + if start_i == 2: + valence = self._idioms_check(valence, words_and_emoticons, i) + + # future work: consider other sentiment-laden idioms + # other_idioms = + # {"back handed": -2, "blow smoke": -2, "blowing smoke": -2, + # "upper hand": 1, "break a leg": 2, + # "cooking with gas": 2, "in the black": 2, "in the red": -2, + # "on the ball": 2,"under the weather": -2} + + valence = self._least_check(valence, words_and_emoticons, i) + + sentiments.append(valence) + return sentiments + + def _least_check(self, valence, words_and_emoticons, i): + # check for negation case using "least" + if ( + i > 1 + and words_and_emoticons[i - 1].lower() not in self.lexicon + and words_and_emoticons[i - 1].lower() == "least" + ): + if ( + words_and_emoticons[i - 2].lower() != "at" + and words_and_emoticons[i - 2].lower() != "very" + ): + valence = valence * self.constants.N_SCALAR + elif ( + i > 0 + and words_and_emoticons[i - 1].lower() not in self.lexicon + and words_and_emoticons[i - 1].lower() == "least" + ): + valence = valence * self.constants.N_SCALAR + return valence + + def _but_check(self, words_and_emoticons, sentiments): + words_and_emoticons = [w_e.lower() for w_e in words_and_emoticons] + but = {"but"} & set(words_and_emoticons) + if but: + bi = words_and_emoticons.index(next(iter(but))) + for sidx, sentiment in enumerate(sentiments): + if sidx < bi: + sentiments[sidx] = sentiment * 0.5 + elif sidx > bi: + sentiments[sidx] = sentiment * 1.5 + return sentiments + + def _idioms_check(self, valence, words_and_emoticons, i): + onezero = f"{words_and_emoticons[i - 1]} {words_and_emoticons[i]}" + + twoonezero = "{} {} {}".format( + words_and_emoticons[i - 2], + words_and_emoticons[i - 1], + words_and_emoticons[i], + ) + + twoone = f"{words_and_emoticons[i - 2]} {words_and_emoticons[i - 1]}" + + threetwoone = "{} {} {}".format( + words_and_emoticons[i - 3], + words_and_emoticons[i - 2], + words_and_emoticons[i - 1], + ) + + threetwo = "{} {}".format( + words_and_emoticons[i - 3], words_and_emoticons[i - 2] + ) + + sequences = [onezero, twoonezero, twoone, threetwoone, threetwo] + + for seq in sequences: + if seq in self.constants.SPECIAL_CASE_IDIOMS: + valence = self.constants.SPECIAL_CASE_IDIOMS[seq] + break + + if len(words_and_emoticons) - 1 > i: + zeroone = f"{words_and_emoticons[i]} {words_and_emoticons[i + 1]}" + if zeroone in self.constants.SPECIAL_CASE_IDIOMS: + valence = self.constants.SPECIAL_CASE_IDIOMS[zeroone] + if len(words_and_emoticons) - 1 > i + 1: + zeroonetwo = "{} {} {}".format( + words_and_emoticons[i], + words_and_emoticons[i + 1], + words_and_emoticons[i + 2], + ) + if zeroonetwo in self.constants.SPECIAL_CASE_IDIOMS: + valence = self.constants.SPECIAL_CASE_IDIOMS[zeroonetwo] + + # check for booster/dampener bi-grams such as 'sort of' or 'kind of' + if ( + threetwo in self.constants.BOOSTER_DICT + or twoone in self.constants.BOOSTER_DICT + ): + valence = valence + self.constants.B_DECR + return valence + + def _never_check(self, valence, words_and_emoticons, start_i, i): + if start_i == 0: + if self.constants.negated([words_and_emoticons[i - 1]]): + valence = valence * self.constants.N_SCALAR + if start_i == 1: + if words_and_emoticons[i - 2] == "never" and ( + words_and_emoticons[i - 1] == "so" + or words_and_emoticons[i - 1] == "this" + ): + valence = valence * 1.5 + elif self.constants.negated([words_and_emoticons[i - (start_i + 1)]]): + valence = valence * self.constants.N_SCALAR + if start_i == 2: + if ( + words_and_emoticons[i - 3] == "never" + and ( + words_and_emoticons[i - 2] == "so" + or words_and_emoticons[i - 2] == "this" + ) + or ( + words_and_emoticons[i - 1] == "so" + or words_and_emoticons[i - 1] == "this" + ) + ): + valence = valence * 1.25 + elif self.constants.negated([words_and_emoticons[i - (start_i + 1)]]): + valence = valence * self.constants.N_SCALAR + return valence + + def _punctuation_emphasis(self, sum_s, text): + # add emphasis from exclamation points and question marks + ep_amplifier = self._amplify_ep(text) + qm_amplifier = self._amplify_qm(text) + punct_emph_amplifier = ep_amplifier + qm_amplifier + return punct_emph_amplifier + + def _amplify_ep(self, text): + # check for added emphasis resulting from exclamation points (up to 4 of them) + ep_count = text.count("!") + if ep_count > 4: + ep_count = 4 + # (empirically derived mean sentiment intensity rating increase for + # exclamation points) + ep_amplifier = ep_count * 0.292 + return ep_amplifier + + def _amplify_qm(self, text): + # check for added emphasis resulting from question marks (2 or 3+) + qm_count = text.count("?") + qm_amplifier = 0 + if qm_count > 1: + if qm_count <= 3: + # (empirically derived mean sentiment intensity rating increase for + # question marks) + qm_amplifier = qm_count * 0.18 + else: + qm_amplifier = 0.96 + return qm_amplifier + + def _sift_sentiment_scores(self, sentiments): + # want separate positive versus negative sentiment scores + pos_sum = 0.0 + neg_sum = 0.0 + neu_count = 0 + for sentiment_score in sentiments: + if sentiment_score > 0: + pos_sum += ( + float(sentiment_score) + 1 + ) # compensates for neutral words that are counted as 1 + if sentiment_score < 0: + neg_sum += ( + float(sentiment_score) - 1 + ) # when used with math.fabs(), compensates for neutrals + if sentiment_score == 0: + neu_count += 1 + return pos_sum, neg_sum, neu_count + + def score_valence(self, sentiments, text): + if sentiments: + sum_s = float(sum(sentiments)) + # compute and add emphasis from punctuation in text + punct_emph_amplifier = self._punctuation_emphasis(sum_s, text) + if sum_s > 0: + sum_s += punct_emph_amplifier + elif sum_s < 0: + sum_s -= punct_emph_amplifier + + compound = self.constants.normalize(sum_s) + # discriminate between positive, negative and neutral sentiment scores + pos_sum, neg_sum, neu_count = self._sift_sentiment_scores(sentiments) + + if pos_sum > math.fabs(neg_sum): + pos_sum += punct_emph_amplifier + elif pos_sum < math.fabs(neg_sum): + neg_sum -= punct_emph_amplifier + + total = pos_sum + math.fabs(neg_sum) + neu_count + pos = math.fabs(pos_sum / total) + neg = math.fabs(neg_sum / total) + neu = math.fabs(neu_count / total) + + else: + compound = 0.0 + pos = 0.0 + neg = 0.0 + neu = 0.0 + + sentiment_dict = { + "neg": round(neg, 3), + "neu": round(neu, 3), + "pos": round(pos, 3), + "compound": round(compound, 4), + } + + return sentiment_dict diff --git a/nltk/stem/__init__.py b/nltk/stem/__init__.py new file mode 100644 index 0000000..1825d85 --- /dev/null +++ b/nltk/stem/__init__.py @@ -0,0 +1,34 @@ +# Natural Language Toolkit: Stemmers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +NLTK Stemmers + +Interfaces used to remove morphological affixes from words, leaving +only the word stem. Stemming algorithms aim to remove those affixes +required for eg. grammatical role, tense, derivational morphology +leaving only the stem of the word. This is a difficult problem due to +irregular words (eg. common verbs in English), complicated +morphological rules, and part-of-speech and sense ambiguities +(eg. ``ceil-`` is not the stem of ``ceiling``). + +StemmerI defines a standard interface for stemmers. +""" + +from nltk.stem.api import StemmerI +from nltk.stem.arlstem import ARLSTem +from nltk.stem.arlstem2 import ARLSTem2 +from nltk.stem.cistem import Cistem +from nltk.stem.isri import ISRIStemmer +from nltk.stem.lancaster import LancasterStemmer +from nltk.stem.porter import PorterStemmer +from nltk.stem.regexp import RegexpStemmer +from nltk.stem.rslp import RSLPStemmer +from nltk.stem.snowball import SnowballStemmer +from nltk.stem.wordnet import WordNetLemmatizer diff --git a/nltk/stem/api.py b/nltk/stem/api.py new file mode 100644 index 0000000..4bafaba --- /dev/null +++ b/nltk/stem/api.py @@ -0,0 +1,27 @@ +# Natural Language Toolkit: Stemmer Interface +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from abc import ABCMeta, abstractmethod + + +class StemmerI(metaclass=ABCMeta): + """ + A processing interface for removing morphological affixes from + words. This process is known as stemming. + + """ + + @abstractmethod + def stem(self, token): + """ + Strip affixes from the token and return the stem. + + :param token: The token that should be stemmed. + :type token: str + """ diff --git a/nltk/stem/arlstem.py b/nltk/stem/arlstem.py new file mode 100644 index 0000000..4fbfd0a --- /dev/null +++ b/nltk/stem/arlstem.py @@ -0,0 +1,361 @@ +# +# Natural Language Toolkit: ARLSTem Stemmer +# +# Copyright (C) 2001-2026 NLTK Project +# +# Author: Kheireddine Abainia (x-programer) +# Algorithms: Kheireddine Abainia +# Siham Ouamour +# Halim Sayoud +# URL: +# For license information, see LICENSE.TXT + + +""" +ARLSTem Arabic Stemmer +The details about the implementation of this algorithm are described in: +K. Abainia, S. Ouamour and H. Sayoud, A Novel Robust Arabic Light Stemmer , +Journal of Experimental & Theoretical Artificial Intelligence (JETAI'17), +Vol. 29, No. 3, 2017, pp. 557-573. +The ARLSTem is a light Arabic stemmer that is based on removing the affixes +from the word (i.e. prefixes, suffixes and infixes). It was evaluated and +compared to several other stemmers using Paice's parameters (under-stemming +index, over-stemming index and stemming weight), and the results showed that +ARLSTem is promising and producing high performances. This stemmer is not +based on any dictionary and can be used on-line effectively. +""" +import re + +from nltk.stem.api import StemmerI + + +class ARLSTem(StemmerI): + """ + ARLSTem stemmer : a light Arabic Stemming algorithm without any dictionary. + Department of Telecommunication & Information Processing. USTHB University, + Algiers, Algeria. + ARLSTem.stem(token) returns the Arabic stem for the input token. + The ARLSTem Stemmer requires that all tokens are encoded using Unicode + encoding. + """ + + def __init__(self): + # different Alif with hamza + self.re_hamzated_alif = re.compile(r"[\u0622\u0623\u0625]") + self.re_alifMaqsura = re.compile(r"[\u0649]") + self.re_diacritics = re.compile(r"[\u064B-\u065F]") + + # Alif Laam, Laam Laam, Fa Laam, Fa Ba + self.pr2 = ["\u0627\u0644", "\u0644\u0644", "\u0641\u0644", "\u0641\u0628"] + # Ba Alif Laam, Kaaf Alif Laam, Waaw Alif Laam + self.pr3 = ["\u0628\u0627\u0644", "\u0643\u0627\u0644", "\u0648\u0627\u0644"] + # Fa Laam Laam, Waaw Laam Laam + self.pr32 = ["\u0641\u0644\u0644", "\u0648\u0644\u0644"] + # Fa Ba Alif Laam, Waaw Ba Alif Laam, Fa Kaaf Alif Laam + self.pr4 = [ + "\u0641\u0628\u0627\u0644", + "\u0648\u0628\u0627\u0644", + "\u0641\u0643\u0627\u0644", + ] + + # Kaf Yaa, Kaf Miim + self.su2 = ["\u0643\u064a", "\u0643\u0645"] + # Ha Alif, Ha Miim + self.su22 = ["\u0647\u0627", "\u0647\u0645"] + # Kaf Miim Alif, Kaf Noon Shadda + self.su3 = ["\u0643\u0645\u0627", "\u0643\u0646\u0651"] + # Ha Miim Alif, Ha Noon Shadda + self.su32 = ["\u0647\u0645\u0627", "\u0647\u0646\u0651"] + + # Alif Noon, Ya Noon, Waaw Noon + self.pl_si2 = ["\u0627\u0646", "\u064a\u0646", "\u0648\u0646"] + # Taa Alif Noon, Taa Ya Noon + self.pl_si3 = ["\u062a\u0627\u0646", "\u062a\u064a\u0646"] + + # Alif Noon, Waaw Noon + self.verb_su2 = ["\u0627\u0646", "\u0648\u0646"] + # Siin Taa, Siin Yaa + self.verb_pr2 = ["\u0633\u062a", "\u0633\u064a"] + # Siin Alif, Siin Noon + self.verb_pr22 = ["\u0633\u0627", "\u0633\u0646"] + # Lam Noon, Lam Taa, Lam Yaa, Lam Hamza + self.verb_pr33 = [ + "\u0644\u0646", + "\u0644\u062a", + "\u0644\u064a", + "\u0644\u0623", + ] + # Taa Miim Alif, Taa Noon Shadda + self.verb_suf3 = ["\u062a\u0645\u0627", "\u062a\u0646\u0651"] + # Noon Alif, Taa Miim, Taa Alif, Waaw Alif + self.verb_suf2 = [ + "\u0646\u0627", + "\u062a\u0645", + "\u062a\u0627", + "\u0648\u0627", + ] + # Taa, Alif, Noon + self.verb_suf1 = ["\u062a", "\u0627", "\u0646"] + + def stem(self, token): + """ + call this function to get the word's stem based on ARLSTem . + """ + try: + if token is None: + raise ValueError( + "The word could not be stemmed, because \ + it is empty !" + ) + # remove Arabic diacritics and replace some letters with others + token = self.norm(token) + # strip common prefixes of the nouns + pre = self.pref(token) + if pre is not None: + token = pre + # strip the suffixes which are common to nouns and verbs + token = self.suff(token) + # transform a plural noun to a singular noun + ps = self.plur2sing(token) + if ps is None: + # transform from the feminine form to the masculine form + fm = self.fem2masc(token) + if fm is not None: + return fm + else: + if pre is None: # if the prefixes are not stripped + # strip the verb prefixes and suffixes + return self.verb(token) + else: + return ps + return token + except ValueError as e: + print(e) + + def norm(self, token): + """ + normalize the word by removing diacritics, replacing hamzated Alif + with Alif replacing AlifMaqsura with Yaa and removing Waaw at the + beginning. + """ + # strip Arabic diacritics + token = self.re_diacritics.sub("", token) + # replace Hamzated Alif with Alif bare + token = self.re_hamzated_alif.sub("\u0627", token) + # replace alifMaqsura with Yaa + token = self.re_alifMaqsura.sub("\u064a", token) + # strip the Waaw from the word beginning if the remaining is 3 letters + # at least + if token.startswith("\u0648") and len(token) > 3: + token = token[1:] + return token + + def pref(self, token): + """ + remove prefixes from the words' beginning. + """ + if len(token) > 5: + for p3 in self.pr3: + if token.startswith(p3): + return token[3:] + if len(token) > 6: + for p4 in self.pr4: + if token.startswith(p4): + return token[4:] + if len(token) > 5: + for p3 in self.pr32: + if token.startswith(p3): + return token[3:] + if len(token) > 4: + for p2 in self.pr2: + if token.startswith(p2): + return token[2:] + + def suff(self, token): + """ + remove suffixes from the word's end. + """ + if token.endswith("\u0643") and len(token) > 3: + return token[:-1] + if len(token) > 4: + for s2 in self.su2: + if token.endswith(s2): + return token[:-2] + if len(token) > 5: + for s3 in self.su3: + if token.endswith(s3): + return token[:-3] + if token.endswith("\u0647") and len(token) > 3: + token = token[:-1] + return token + if len(token) > 4: + for s2 in self.su22: + if token.endswith(s2): + return token[:-2] + if len(token) > 5: + for s3 in self.su32: + if token.endswith(s3): + return token[:-3] + if token.endswith("\u0646\u0627") and len(token) > 4: + return token[:-2] + return token + + def fem2masc(self, token): + """ + transform the word from the feminine form to the masculine form. + """ + if token.endswith("\u0629") and len(token) > 3: + return token[:-1] + + def plur2sing(self, token): + """ + transform the word from the plural form to the singular form. + """ + if len(token) > 4: + for ps2 in self.pl_si2: + if token.endswith(ps2): + return token[:-2] + if len(token) > 5: + for ps3 in self.pl_si3: + if token.endswith(ps3): + return token[:-3] + if len(token) > 3 and token.endswith("\u0627\u062a"): + return token[:-2] + if len(token) > 3 and token.startswith("\u0627") and token[2] == "\u0627": + return token[:2] + token[3:] + if len(token) > 4 and token.startswith("\u0627") and token[-2] == "\u0627": + return token[1:-2] + token[-1] + + def verb(self, token): + """ + stem the verb prefixes and suffixes or both + """ + vb = self.verb_t1(token) + if vb is not None: + return vb + vb = self.verb_t2(token) + if vb is not None: + return vb + vb = self.verb_t3(token) + if vb is not None: + return vb + vb = self.verb_t4(token) + if vb is not None: + return vb + vb = self.verb_t5(token) + if vb is not None: + return vb + return self.verb_t6(token) + + def verb_t1(self, token): + """ + stem the present prefixes and suffixes + """ + if len(token) > 5 and token.startswith("\u062a"): # Taa + for s2 in self.pl_si2: + if token.endswith(s2): + return token[1:-2] + if len(token) > 5 and token.startswith("\u064a"): # Yaa + for s2 in self.verb_su2: + if token.endswith(s2): + return token[1:-2] + if len(token) > 4 and token.startswith("\u0627"): # Alif + # Waaw Alif + if len(token) > 5 and token.endswith("\u0648\u0627"): + return token[1:-2] + # Yaa + if token.endswith("\u064a"): + return token[1:-1] + # Alif + if token.endswith("\u0627"): + return token[1:-1] + # Noon + if token.endswith("\u0646"): + return token[1:-1] + # ^Yaa, Noon$ + if len(token) > 4 and token.startswith("\u064a") and token.endswith("\u0646"): + return token[1:-1] + # ^Taa, Noon$ + if len(token) > 4 and token.startswith("\u062a") and token.endswith("\u0646"): + return token[1:-1] + + def verb_t2(self, token): + """ + stem the future prefixes and suffixes + """ + if len(token) > 6: + for s2 in self.pl_si2: + # ^Siin Taa + if token.startswith(self.verb_pr2[0]) and token.endswith(s2): + return token[2:-2] + # ^Siin Yaa, Alif Noon$ + if token.startswith(self.verb_pr2[1]) and token.endswith(self.pl_si2[0]): + return token[2:-2] + # ^Siin Yaa, Waaw Noon$ + if token.startswith(self.verb_pr2[1]) and token.endswith(self.pl_si2[2]): + return token[2:-2] + # ^Siin Taa, Noon$ + if ( + len(token) > 5 + and token.startswith(self.verb_pr2[0]) + and token.endswith("\u0646") + ): + return token[2:-1] + # ^Siin Yaa, Noon$ + if ( + len(token) > 5 + and token.startswith(self.verb_pr2[1]) + and token.endswith("\u0646") + ): + return token[2:-1] + + def verb_t3(self, token): + """ + stem the present suffixes + """ + if len(token) > 5: + for su3 in self.verb_suf3: + if token.endswith(su3): + return token[:-3] + if len(token) > 4: + for su2 in self.verb_suf2: + if token.endswith(su2): + return token[:-2] + if len(token) > 3: + for su1 in self.verb_suf1: + if token.endswith(su1): + return token[:-1] + + def verb_t4(self, token): + """ + stem the present prefixes + """ + if len(token) > 3: + for pr1 in self.verb_suf1: + if token.startswith(pr1): + return token[1:] + if token.startswith("\u064a"): + return token[1:] + + def verb_t5(self, token): + """ + stem the future prefixes + """ + if len(token) > 4: + for pr2 in self.verb_pr22: + if token.startswith(pr2): + return token[2:] + for pr2 in self.verb_pr2: + if token.startswith(pr2): + return token[2:] + return token + + def verb_t6(self, token): + """ + stem the order prefixes + """ + if len(token) > 4: + for pr3 in self.verb_pr33: + if token.startswith(pr3): + return token[2:] + return token diff --git a/nltk/stem/arlstem2.py b/nltk/stem/arlstem2.py new file mode 100644 index 0000000..a5b6b82 --- /dev/null +++ b/nltk/stem/arlstem2.py @@ -0,0 +1,457 @@ +# +# Natural Language Toolkit: ARLSTem Stemmer v2 +# +# Copyright (C) 2001-2026 NLTK Project +# +# Author: Kheireddine Abainia (x-programer) +# Algorithms: Kheireddine Abainia +# Hamza Rebbani +# URL: +# For license information, see LICENSE.TXT + + +""" +ARLSTem2 Arabic Light Stemmer +The details about the implementation of this algorithm are described in: +K. Abainia and H. Rebbani, Comparing the Effectiveness of the Improved ARLSTem +Algorithm with Existing Arabic Light Stemmers, International Conference on +Theoretical and Applicative Aspects of Computer Science (ICTAACS'19), Skikda, +Algeria, December 15-16, 2019. +ARLSTem2 is an Arabic light stemmer based on removing the affixes from +the words (i.e. prefixes, suffixes and infixes). It is an improvement +of the previous Arabic light stemmer (ARLSTem). The new version was compared to +the original algorithm and several existing Arabic light stemmers, where the +results showed that the new version considerably improves the under-stemming +errors that are common to light stemmers. Both ARLSTem and ARLSTem2 can be run +online and do not use any dictionary. +""" +import re + +from nltk.stem.api import StemmerI + + +class ARLSTem2(StemmerI): + """ + Return a stemmed Arabic word after removing affixes. This an improved + version of the previous algorithm, which reduces under-stemming errors. + Typically used in Arabic search engine, information retrieval and NLP. + + >>> from nltk.stem import arlstem2 + >>> stemmer = ARLSTem2() + >>> word = stemmer.stem('يعمل') + >>> print(word) + عمل + + :param token: The input Arabic word (unicode) to be stemmed + :type token: unicode + :return: A unicode Arabic word + """ + + def __init__(self): + # different Alif with hamza + self.re_hamzated_alif = re.compile(r"[\u0622\u0623\u0625]") + self.re_alifMaqsura = re.compile(r"[\u0649]") + self.re_diacritics = re.compile(r"[\u064B-\u065F]") + + # Alif Laam, Laam Laam, Fa Laam, Fa Ba + self.pr2 = ["\u0627\u0644", "\u0644\u0644", "\u0641\u0644", "\u0641\u0628"] + # Ba Alif Laam, Kaaf Alif Laam, Waaw Alif Laam + self.pr3 = ["\u0628\u0627\u0644", "\u0643\u0627\u0644", "\u0648\u0627\u0644"] + # Fa Laam Laam, Waaw Laam Laam + self.pr32 = ["\u0641\u0644\u0644", "\u0648\u0644\u0644"] + # Fa Ba Alif Laam, Waaw Ba Alif Laam, Fa Kaaf Alif Laam + self.pr4 = [ + "\u0641\u0628\u0627\u0644", + "\u0648\u0628\u0627\u0644", + "\u0641\u0643\u0627\u0644", + ] + + # Kaf Yaa, Kaf Miim + self.su2 = ["\u0643\u064a", "\u0643\u0645"] + # Ha Alif, Ha Miim + self.su22 = ["\u0647\u0627", "\u0647\u0645"] + # Kaf Miim Alif, Kaf Noon Shadda + self.su3 = ["\u0643\u0645\u0627", "\u0643\u0646\u0651"] + # Ha Miim Alif, Ha Noon Shadda + self.su32 = ["\u0647\u0645\u0627", "\u0647\u0646\u0651"] + + # Alif Noon, Ya Noon, Waaw Noon + self.pl_si2 = ["\u0627\u0646", "\u064a\u0646", "\u0648\u0646"] + # Taa Alif Noon, Taa Ya Noon + self.pl_si3 = ["\u062a\u0627\u0646", "\u062a\u064a\u0646"] + + # Alif Noon, Waaw Noon + self.verb_su2 = ["\u0627\u0646", "\u0648\u0646"] + # Siin Taa, Siin Yaa + self.verb_pr2 = ["\u0633\u062a", "\u0633\u064a"] + # Siin Alif, Siin Noon + self.verb_pr22 = ["\u0633\u0627", "\u0633\u0646"] + # Lam Noon, Lam Taa, Lam Yaa, Lam Hamza + self.verb_pr33 = [ + "\u0644\u0646", + "\u0644\u062a", + "\u0644\u064a", + "\u0644\u0623", + ] + # Taa Miim Alif, Taa Noon Shadda + self.verb_suf3 = ["\u062a\u0645\u0627", "\u062a\u0646\u0651"] + # Noon Alif, Taa Miim, Taa Alif, Waaw Alif + self.verb_suf2 = [ + "\u0646\u0627", + "\u062a\u0645", + "\u062a\u0627", + "\u0648\u0627", + ] + # Taa, Alif, Noon + self.verb_suf1 = ["\u062a", "\u0627", "\u0646"] + + def stem1(self, token): + """ + call this function to get the first stem + """ + try: + if token is None: + raise ValueError( + "The word could not be stemmed, because \ + it is empty !" + ) + self.is_verb = False + # remove Arabic diacritics and replace some letters with others + token = self.norm(token) + # strip the common noun prefixes + pre = self.pref(token) + if pre is not None: + token = pre + # transform the feminine form to masculine form + fm = self.fem2masc(token) + if fm is not None: + return fm + # strip the adjective affixes + adj = self.adjective(token) + if adj is not None: + return adj + # strip the suffixes that are common to nouns and verbs + token = self.suff(token) + # transform a plural noun to a singular noun + ps = self.plur2sing(token) + if ps is None: + if pre is None: # if the noun prefixes are not stripped + # strip the verb prefixes and suffixes + verb = self.verb(token) + if verb is not None: + self.is_verb = True + return verb + else: + return ps + return token + except ValueError as e: + print(e) + + def stem(self, token): + # stem the input word + try: + if token is None: + raise ValueError( + "The word could not be stemmed, because \ + it is empty !" + ) + # run the first round of stemming + token = self.stem1(token) + # check if there is some additional noun affixes + if len(token) > 4: + # ^Taa, $Yaa + char + if token.startswith("\u062a") and token[-2] == "\u064a": + token = token[1:-2] + token[-1] + return token + # ^Miim, $Waaw + char + if token.startswith("\u0645") and token[-2] == "\u0648": + token = token[1:-2] + token[-1] + return token + if len(token) > 3: + # !^Alif, $Yaa + if not token.startswith("\u0627") and token.endswith("\u064a"): + token = token[:-1] + return token + # $Laam + if token.startswith("\u0644"): + return token[1:] + return token + except ValueError as e: + print(e) + + def norm(self, token): + """ + normalize the word by removing diacritics, replace hamzated Alif + with Alif bare, replace AlifMaqsura with Yaa and remove Waaw at the + beginning. + """ + # strip Arabic diacritics + token = self.re_diacritics.sub("", token) + # replace Hamzated Alif with Alif bare + token = self.re_hamzated_alif.sub("\u0627", token) + # replace alifMaqsura with Yaa + token = self.re_alifMaqsura.sub("\u064a", token) + # strip the Waaw from the word beginning if the remaining is + # tri-literal at least + if token.startswith("\u0648") and len(token) > 3: + token = token[1:] + return token + + def pref(self, token): + """ + remove prefixes from the words' beginning. + """ + if len(token) > 5: + for p3 in self.pr3: + if token.startswith(p3): + return token[3:] + if len(token) > 6: + for p4 in self.pr4: + if token.startswith(p4): + return token[4:] + if len(token) > 5: + for p3 in self.pr32: + if token.startswith(p3): + return token[3:] + if len(token) > 4: + for p2 in self.pr2: + if token.startswith(p2): + return token[2:] + + def adjective(self, token): + """ + remove the infixes from adjectives + """ + # ^Alif, Alif, $Yaa + if len(token) > 5: + if ( + token.startswith("\u0627") + and token[-3] == "\u0627" + and token.endswith("\u064a") + ): + return token[:-3] + token[-2] + + def suff(self, token): + """ + remove the suffixes from the word's ending. + """ + if token.endswith("\u0643") and len(token) > 3: + return token[:-1] + if len(token) > 4: + for s2 in self.su2: + if token.endswith(s2): + return token[:-2] + if len(token) > 5: + for s3 in self.su3: + if token.endswith(s3): + return token[:-3] + if token.endswith("\u0647") and len(token) > 3: + token = token[:-1] + return token + if len(token) > 4: + for s2 in self.su22: + if token.endswith(s2): + return token[:-2] + if len(token) > 5: + for s3 in self.su32: + if token.endswith(s3): + return token[:-3] + # $Noon and Alif + if token.endswith("\u0646\u0627") and len(token) > 4: + return token[:-2] + return token + + def fem2masc(self, token): + """ + transform the word from the feminine form to the masculine form. + """ + if len(token) > 6: + # ^Taa, Yaa, $Yaa and Taa Marbuta + if ( + token.startswith("\u062a") + and token[-4] == "\u064a" + and token.endswith("\u064a\u0629") + ): + return token[1:-4] + token[-3] + # ^Alif, Yaa, $Yaa and Taa Marbuta + if ( + token.startswith("\u0627") + and token[-4] == "\u0627" + and token.endswith("\u064a\u0629") + ): + return token[:-4] + token[-3] + # $Alif, Yaa and Taa Marbuta + if token.endswith("\u0627\u064a\u0629") and len(token) > 5: + return token[:-2] + if len(token) > 4: + # Alif, $Taa Marbuta + if token[1] == "\u0627" and token.endswith("\u0629"): + return token[0] + token[2:-1] + # $Yaa and Taa Marbuta + if token.endswith("\u064a\u0629"): + return token[:-2] + # $Taa Marbuta + if token.endswith("\u0629") and len(token) > 3: + return token[:-1] + + def plur2sing(self, token): + """ + transform the word from the plural form to the singular form. + """ + # ^Haa, $Noon, Waaw + if len(token) > 5: + if token.startswith("\u0645") and token.endswith("\u0648\u0646"): + return token[1:-2] + if len(token) > 4: + for ps2 in self.pl_si2: + if token.endswith(ps2): + return token[:-2] + if len(token) > 5: + for ps3 in self.pl_si3: + if token.endswith(ps3): + return token[:-3] + if len(token) > 4: + # $Alif, Taa + if token.endswith("\u0627\u062a"): + return token[:-2] + # ^Alif Alif + if token.startswith("\u0627") and token[2] == "\u0627": + return token[:2] + token[3:] + # ^Alif Alif + if token.startswith("\u0627") and token[-2] == "\u0627": + return token[1:-2] + token[-1] + + def verb(self, token): + """ + stem the verb prefixes and suffixes or both + """ + vb = self.verb_t1(token) + if vb is not None: + return vb + vb = self.verb_t2(token) + if vb is not None: + return vb + vb = self.verb_t3(token) + if vb is not None: + return vb + vb = self.verb_t4(token) + if vb is not None: + return vb + vb = self.verb_t5(token) + if vb is not None: + return vb + vb = self.verb_t6(token) + return vb + + def verb_t1(self, token): + """ + stem the present tense co-occurred prefixes and suffixes + """ + if len(token) > 5 and token.startswith("\u062a"): # Taa + for s2 in self.pl_si2: + if token.endswith(s2): + return token[1:-2] + if len(token) > 5 and token.startswith("\u064a"): # Yaa + for s2 in self.verb_su2: + if token.endswith(s2): + return token[1:-2] + if len(token) > 4 and token.startswith("\u0627"): # Alif + # Waaw Alif + if len(token) > 5 and token.endswith("\u0648\u0627"): + return token[1:-2] + # Yaa + if token.endswith("\u064a"): + return token[1:-1] + # Alif + if token.endswith("\u0627"): + return token[1:-1] + # Noon + if token.endswith("\u0646"): + return token[1:-1] + # ^Yaa, Noon$ + if len(token) > 4 and token.startswith("\u064a") and token.endswith("\u0646"): + return token[1:-1] + # ^Taa, Noon$ + if len(token) > 4 and token.startswith("\u062a") and token.endswith("\u0646"): + return token[1:-1] + + def verb_t2(self, token): + """ + stem the future tense co-occurred prefixes and suffixes + """ + if len(token) > 6: + for s2 in self.pl_si2: + # ^Siin Taa + if token.startswith(self.verb_pr2[0]) and token.endswith(s2): + return token[2:-2] + # ^Siin Yaa, Alif Noon$ + if token.startswith(self.verb_pr2[1]) and token.endswith(self.pl_si2[0]): + return token[2:-2] + # ^Siin Yaa, Waaw Noon$ + if token.startswith(self.verb_pr2[1]) and token.endswith(self.pl_si2[2]): + return token[2:-2] + # ^Siin Taa, Noon$ + if ( + len(token) > 5 + and token.startswith(self.verb_pr2[0]) + and token.endswith("\u0646") + ): + return token[2:-1] + # ^Siin Yaa, Noon$ + if ( + len(token) > 5 + and token.startswith(self.verb_pr2[1]) + and token.endswith("\u0646") + ): + return token[2:-1] + + def verb_t3(self, token): + """ + stem the present tense suffixes + """ + if len(token) > 5: + for su3 in self.verb_suf3: + if token.endswith(su3): + return token[:-3] + if len(token) > 4: + for su2 in self.verb_suf2: + if token.endswith(su2): + return token[:-2] + if len(token) > 3: + for su1 in self.verb_suf1: + if token.endswith(su1): + return token[:-1] + + def verb_t4(self, token): + """ + stem the present tense prefixes + """ + if len(token) > 3: + for pr1 in self.verb_suf1: + if token.startswith(pr1): + return token[1:] + if token.startswith("\u064a"): + return token[1:] + + def verb_t5(self, token): + """ + stem the future tense prefixes + """ + if len(token) > 4: + for pr2 in self.verb_pr22: + if token.startswith(pr2): + return token[2:] + for pr2 in self.verb_pr2: + if token.startswith(pr2): + return token[2:] + + def verb_t6(self, token): + """ + stem the imperative tense prefixes + """ + if len(token) > 4: + for pr3 in self.verb_pr33: + if token.startswith(pr3): + return token[2:] + + return token diff --git a/nltk/stem/cistem.py b/nltk/stem/cistem.py new file mode 100644 index 0000000..a834222 --- /dev/null +++ b/nltk/stem/cistem.py @@ -0,0 +1,234 @@ +# Natural Language Toolkit: CISTEM Stemmer for German +# Copyright (C) 2001-2026 NLTK Project +# Author: Leonie Weissweiler +# Tom Aarsen <> (modifications) +# Algorithm: Leonie Weissweiler +# Alexander Fraser +# URL: +# For license information, see LICENSE.TXT + +import re +from typing import Tuple + +from nltk.stem.api import StemmerI + + +class Cistem(StemmerI): + """ + CISTEM Stemmer for German + + This is the official Python implementation of the CISTEM stemmer. + It is based on the paper + Leonie Weissweiler, Alexander Fraser (2017). Developing a Stemmer for German + Based on a Comparative Analysis of Publicly Available Stemmers. + In Proceedings of the German Society for Computational Linguistics and Language + Technology (GSCL) + which can be read here: + https://www.cis.lmu.de/~weissweiler/cistem/ + + In the paper, we conducted an analysis of publicly available stemmers, + developed two gold standards for German stemming and evaluated the stemmers + based on the two gold standards. We then proposed the stemmer implemented here + and show that it achieves slightly better f-measure than the other stemmers and + is thrice as fast as the Snowball stemmer for German while being about as fast + as most other stemmers. + + case_insensitive is a a boolean specifying if case-insensitive stemming + should be used. Case insensitivity improves performance only if words in the + text may be incorrectly upper case. For all-lowercase and correctly cased + text, best performance is achieved by setting case_insensitive for false. + + :param case_insensitive: if True, the stemming is case insensitive. False by default. + :type case_insensitive: bool + """ + + strip_ge = re.compile(r"^ge(.{4,})") + repl_xx = re.compile(r"(.)\1") + repl_xx_back = re.compile(r"(.)\*") + # The end-anchored suffix patterns (e[mr]$, nd$, t$, [esn]$) are applied by + # direct end-of-string character checks in ``_segment_inner`` (see there), + # not via ``re``, so that stemming is linear rather than quadratic in the + # word length (CWE-770; CVE-2026-12868). + + def __init__(self, case_insensitive: bool = False): + self._case_insensitive = case_insensitive + + @staticmethod + def replace_to(word: str) -> str: + word = word.replace("sch", "$") + word = word.replace("ei", "%") + word = word.replace("ie", "&") + word = Cistem.repl_xx.sub(r"\1*", word) + + return word + + @staticmethod + def replace_back(word: str) -> str: + word = Cistem.repl_xx_back.sub(r"\1\1", word) + word = word.replace("%", "ei") + word = word.replace("&", "ie") + word = word.replace("$", "sch") + + return word + + def stem(self, word: str) -> str: + """Stems the input word. + + :param word: The word that is to be stemmed. + :type word: str + :return: The stemmed word. + :rtype: str + + >>> from nltk.stem.cistem import Cistem + >>> stemmer = Cistem() + >>> s1 = "Speicherbehältern" + >>> stemmer.stem(s1) + 'speicherbehalt' + >>> s2 = "Grenzpostens" + >>> stemmer.stem(s2) + 'grenzpost' + >>> s3 = "Ausgefeiltere" + >>> stemmer.stem(s3) + 'ausgefeilt' + >>> stemmer = Cistem(True) + >>> stemmer.stem(s1) + 'speicherbehal' + >>> stemmer.stem(s2) + 'grenzpo' + >>> stemmer.stem(s3) + 'ausgefeil' + """ + if len(word) == 0: + return word + + upper = word[0].isupper() + word = word.lower() + + word = word.replace("ü", "u") + word = word.replace("ö", "o") + word = word.replace("ä", "a") + word = word.replace("ß", "ss") + + word = Cistem.strip_ge.sub(r"\1", word) + + return self._segment_inner(word, upper)[0] + + def segment(self, word: str) -> tuple[str, str]: + """ + This method works very similarly to stem (:func:'cistem.stem'). The difference is that in + addition to returning the stem, it also returns the rest that was removed at + the end. To be able to return the stem unchanged so the stem and the rest + can be concatenated to form the original word, all subsitutions that altered + the stem in any other way than by removing letters at the end were left out. + + :param word: The word that is to be stemmed. + :type word: str + :return: A tuple of the stemmed word and the removed suffix. + :rtype: Tuple[str, str] + + >>> from nltk.stem.cistem import Cistem + >>> stemmer = Cistem() + >>> s1 = "Speicherbehältern" + >>> stemmer.segment(s1) + ('speicherbehält', 'ern') + >>> s2 = "Grenzpostens" + >>> stemmer.segment(s2) + ('grenzpost', 'ens') + >>> s3 = "Ausgefeiltere" + >>> stemmer.segment(s3) + ('ausgefeilt', 'ere') + >>> stemmer = Cistem(True) + >>> stemmer.segment(s1) + ('speicherbehäl', 'tern') + >>> stemmer.segment(s2) + ('grenzpo', 'stens') + >>> stemmer.segment(s3) + ('ausgefeil', 'tere') + """ + if len(word) == 0: + return ("", "") + + upper = word[0].isupper() + word = word.lower() + + return self._segment_inner(word, upper) + + def _segment_inner(self, word: str, upper: bool): + """Inner method for iteratively applying the code stemming regexes. + This method receives a pre-processed variant of the word to be stemmed, + or the word to be segmented, and returns a tuple of the word and the + removed suffix. + + :param word: A pre-processed variant of the word that is to be stemmed. + :type word: str + :param upper: Whether the original word started with a capital letter. + :type upper: bool + :return: A tuple of the stemmed word and the removed suffix. + :rtype: Tuple[str, str] + """ + + rest_length = 0 + word_copy = word[:] + + # Pre-processing before applying the substitution patterns + word = Cistem.replace_to(word) + rest = "" + + # Apply the substitution patterns. Each pattern (e[mr]$, nd$, t$, [esn]$) + # only ever strips one or two characters anchored at the end of the + # string. The original loop rebuilt the whole remaining string on every + # removal via the compiled ``re`` patterns' ``Pattern.subn`` -- O(n) work + # per step over O(n) steps, i.e. O(n**2) in the word length, so a single + # long word pins a CPU core (CWE-770; CVE-2026-12868). Strip characters + # off the end of a list in O(1) per step instead, joining once at the end. + # + # Python's ``$`` matches at the end of the string *or* just before a + # single trailing newline, so such a newline is transparent to the + # anchored patterns and is preserved (``_strip`` pops it aside and + # restores it); ``j`` is the index of the last character the patterns + # act on. + chars = list(word) + + def _strip(count): + if chars[-1] == "\n": + newline = chars.pop() + del chars[-count:] + chars.append(newline) + else: + del chars[-count:] + + while len(chars) > 3: + j = -2 if chars[-1] == "\n" else -1 + if len(chars) > 5: + if chars[j] in "mr" and chars[j - 1] == "e": # e[mr]$ + _strip(2) + rest_length += 2 + continue + + if chars[j] == "d" and chars[j - 1] == "n": # nd$ + _strip(2) + rest_length += 2 + continue + + if not upper or self._case_insensitive: + if chars[j] == "t": # t$ + _strip(1) + rest_length += 1 + continue + + if chars[j] in "esn": # [esn]$ + _strip(1) + rest_length += 1 + continue + else: + break + + word = "".join(chars) + + # Post-processing after applying the substitution patterns + word = Cistem.replace_back(word) + + if rest_length: + rest = word_copy[-rest_length:] + + return (word, rest) diff --git a/nltk/stem/isri.py b/nltk/stem/isri.py new file mode 100644 index 0000000..8fe3c0e --- /dev/null +++ b/nltk/stem/isri.py @@ -0,0 +1,395 @@ +# +# Natural Language Toolkit: The ISRI Arabic Stemmer +# +# Copyright (C) 2001-2026 NLTK Project +# Algorithm: Kazem Taghva, Rania Elkhoury, and Jeffrey Coombs (2005) +# Author: Hosam Algasaier +# URL: +# For license information, see LICENSE.TXT + +""" +ISRI Arabic Stemmer + +The algorithm for this stemmer is described in: + +Taghva, K., Elkoury, R., and Coombs, J. 2005. Arabic Stemming without a root dictionary. +Information Science Research Institute. University of Nevada, Las Vegas, USA. + +The Information Science Research Institute’s (ISRI) Arabic stemmer shares many features +with the Khoja stemmer. However, the main difference is that ISRI stemmer does not use root +dictionary. Also, if a root is not found, ISRI stemmer returned normalized form, rather than +returning the original unmodified word. + +Additional adjustments were made to improve the algorithm: + +1- Adding 60 stop words. +2- Adding the pattern (تفاعيل) to ISRI pattern set. +3- The step 2 in the original algorithm was normalizing all hamza. This step is discarded because it +increases the word ambiguities and changes the original root. + +""" +import re + +from nltk.stem.api import StemmerI + + +class ISRIStemmer(StemmerI): + """ + ISRI Arabic stemmer based on algorithm: Arabic Stemming without a root dictionary. + Information Science Research Institute. University of Nevada, Las Vegas, USA. + + A few minor modifications have been made to ISRI basic algorithm. + See the source code of this module for more information. + + isri.stem(token) returns Arabic root for the given token. + + The ISRI Stemmer requires that all tokens have Unicode string types. + If you use Python IDLE on Arabic Windows you have to decode text first + using Arabic '1256' coding. + """ + + def __init__(self): + # length three prefixes + self.p3 = [ + "\u0643\u0627\u0644", + "\u0628\u0627\u0644", + "\u0648\u0644\u0644", + "\u0648\u0627\u0644", + ] + + # length two prefixes + self.p2 = ["\u0627\u0644", "\u0644\u0644"] + + # length one prefixes + self.p1 = [ + "\u0644", + "\u0628", + "\u0641", + "\u0633", + "\u0648", + "\u064a", + "\u062a", + "\u0646", + "\u0627", + ] + + # length three suffixes + self.s3 = [ + "\u062a\u0645\u0644", + "\u0647\u0645\u0644", + "\u062a\u0627\u0646", + "\u062a\u064a\u0646", + "\u0643\u0645\u0644", + ] + + # length two suffixes + self.s2 = [ + "\u0648\u0646", + "\u0627\u062a", + "\u0627\u0646", + "\u064a\u0646", + "\u062a\u0646", + "\u0643\u0645", + "\u0647\u0646", + "\u0646\u0627", + "\u064a\u0627", + "\u0647\u0627", + "\u062a\u0645", + "\u0643\u0646", + "\u0646\u064a", + "\u0648\u0627", + "\u0645\u0627", + "\u0647\u0645", + ] + + # length one suffixes + self.s1 = ["\u0629", "\u0647", "\u064a", "\u0643", "\u062a", "\u0627", "\u0646"] + + # groups of length four patterns + self.pr4 = { + 0: ["\u0645"], + 1: ["\u0627"], + 2: ["\u0627", "\u0648", "\u064a"], + 3: ["\u0629"], + } + + # Groups of length five patterns and length three roots + self.pr53 = { + 0: ["\u0627", "\u062a"], + 1: ["\u0627", "\u064a", "\u0648"], + 2: ["\u0627", "\u062a", "\u0645"], + 3: ["\u0645", "\u064a", "\u062a"], + 4: ["\u0645", "\u062a"], + 5: ["\u0627", "\u0648"], + 6: ["\u0627", "\u0645"], + } + + self.re_short_vowels = re.compile(r"[\u064B-\u0652]") + self.re_hamza = re.compile(r"[\u0621\u0624\u0626]") + self.re_initial_hamza = re.compile(r"^[\u0622\u0623\u0625]") + + self.stop_words = [ + "\u064a\u0643\u0648\u0646", + "\u0648\u0644\u064a\u0633", + "\u0648\u0643\u0627\u0646", + "\u0643\u0630\u0644\u0643", + "\u0627\u0644\u062a\u064a", + "\u0648\u0628\u064a\u0646", + "\u0639\u0644\u064a\u0647\u0627", + "\u0645\u0633\u0627\u0621", + "\u0627\u0644\u0630\u064a", + "\u0648\u0643\u0627\u0646\u062a", + "\u0648\u0644\u0643\u0646", + "\u0648\u0627\u0644\u062a\u064a", + "\u062a\u0643\u0648\u0646", + "\u0627\u0644\u064a\u0648\u0645", + "\u0627\u0644\u0644\u0630\u064a\u0646", + "\u0639\u0644\u064a\u0647", + "\u0643\u0627\u0646\u062a", + "\u0644\u0630\u0644\u0643", + "\u0623\u0645\u0627\u0645", + "\u0647\u0646\u0627\u0643", + "\u0645\u0646\u0647\u0627", + "\u0645\u0627\u0632\u0627\u0644", + "\u0644\u0627\u0632\u0627\u0644", + "\u0644\u0627\u064a\u0632\u0627\u0644", + "\u0645\u0627\u064a\u0632\u0627\u0644", + "\u0627\u0635\u0628\u062d", + "\u0623\u0635\u0628\u062d", + "\u0623\u0645\u0633\u0649", + "\u0627\u0645\u0633\u0649", + "\u0623\u0636\u062d\u0649", + "\u0627\u0636\u062d\u0649", + "\u0645\u0627\u0628\u0631\u062d", + "\u0645\u0627\u0641\u062a\u0626", + "\u0645\u0627\u0627\u0646\u0641\u0643", + "\u0644\u0627\u0633\u064a\u0645\u0627", + "\u0648\u0644\u0627\u064a\u0632\u0627\u0644", + "\u0627\u0644\u062d\u0627\u0644\u064a", + "\u0627\u0644\u064a\u0647\u0627", + "\u0627\u0644\u0630\u064a\u0646", + "\u0641\u0627\u0646\u0647", + "\u0648\u0627\u0644\u0630\u064a", + "\u0648\u0647\u0630\u0627", + "\u0644\u0647\u0630\u0627", + "\u0641\u0643\u0627\u0646", + "\u0633\u062a\u0643\u0648\u0646", + "\u0627\u0644\u064a\u0647", + "\u064a\u0645\u0643\u0646", + "\u0628\u0647\u0630\u0627", + "\u0627\u0644\u0630\u0649", + ] + + def stem(self, token): + """ + Stemming a word token using the ISRI stemmer. + """ + token = self.norm( + token, 1 + ) # remove diacritics which representing Arabic short vowels + if token in self.stop_words: + return token # exclude stop words from being processed + token = self.pre32( + token + ) # remove length three and length two prefixes in this order + token = self.suf32( + token + ) # remove length three and length two suffixes in this order + token = self.waw( + token + ) # remove connective ‘و’ if it precedes a word beginning with ‘و’ + token = self.norm(token, 2) # normalize initial hamza to bare alif + # if 4 <= word length <= 7, then stem; otherwise, no stemming + if len(token) == 4: # length 4 word + token = self.pro_w4(token) + elif len(token) == 5: # length 5 word + token = self.pro_w53(token) + token = self.end_w5(token) + elif len(token) == 6: # length 6 word + token = self.pro_w6(token) + token = self.end_w6(token) + elif len(token) == 7: # length 7 word + token = self.suf1(token) + if len(token) == 7: + token = self.pre1(token) + if len(token) == 6: + token = self.pro_w6(token) + token = self.end_w6(token) + return token + + def norm(self, word, num=3): + """ + normalization: + num=1 normalize diacritics + num=2 normalize initial hamza + num=3 both 1&2 + """ + if num == 1: + word = self.re_short_vowels.sub("", word) + elif num == 2: + word = self.re_initial_hamza.sub("\u0627", word) + elif num == 3: + word = self.re_short_vowels.sub("", word) + word = self.re_initial_hamza.sub("\u0627", word) + return word + + def pre32(self, word): + """remove length three and length two prefixes in this order""" + if len(word) >= 6: + for pre3 in self.p3: + if word.startswith(pre3): + return word[3:] + if len(word) >= 5: + for pre2 in self.p2: + if word.startswith(pre2): + return word[2:] + return word + + def suf32(self, word): + """remove length three and length two suffixes in this order""" + if len(word) >= 6: + for suf3 in self.s3: + if word.endswith(suf3): + return word[:-3] + if len(word) >= 5: + for suf2 in self.s2: + if word.endswith(suf2): + return word[:-2] + return word + + def waw(self, word): + """remove connective ‘و’ if it precedes a word beginning with ‘و’""" + if len(word) >= 4 and word[:2] == "\u0648\u0648": + word = word[1:] + return word + + def pro_w4(self, word): + """process length four patterns and extract length three roots""" + if word[0] in self.pr4[0]: # مفعل + word = word[1:] + elif word[1] in self.pr4[1]: # فاعل + word = word[:1] + word[2:] + elif word[2] in self.pr4[2]: # فعال - فعول - فعيل + word = word[:2] + word[3] + elif word[3] in self.pr4[3]: # فعلة + word = word[:-1] + else: + word = self.suf1(word) # do - normalize short sufix + if len(word) == 4: + word = self.pre1(word) # do - normalize short prefix + return word + + def pro_w53(self, word): + """process length five patterns and extract length three roots""" + if word[2] in self.pr53[0] and word[0] == "\u0627": # افتعل - افاعل + word = word[1] + word[3:] + elif word[3] in self.pr53[1] and word[0] == "\u0645": # مفعول - مفعال - مفعيل + word = word[1:3] + word[4] + elif word[0] in self.pr53[2] and word[4] == "\u0629": # مفعلة - تفعلة - افعلة + word = word[1:4] + elif word[0] in self.pr53[3] and word[2] == "\u062a": # مفتعل - يفتعل - تفتعل + word = word[1] + word[3:] + elif word[0] in self.pr53[4] and word[2] == "\u0627": # مفاعل - تفاعل + word = word[1] + word[3:] + elif word[2] in self.pr53[5] and word[4] == "\u0629": # فعولة - فعالة + word = word[:2] + word[3] + elif word[0] in self.pr53[6] and word[1] == "\u0646": # انفعل - منفعل + word = word[2:] + elif word[3] == "\u0627" and word[0] == "\u0627": # افعال + word = word[1:3] + word[4] + elif word[4] == "\u0646" and word[3] == "\u0627": # فعلان + word = word[:3] + elif word[3] == "\u064a" and word[0] == "\u062a": # تفعيل + word = word[1:3] + word[4] + elif word[3] == "\u0648" and word[1] == "\u0627": # فاعول + word = word[0] + word[2] + word[4] + elif word[2] == "\u0627" and word[1] == "\u0648": # فواعل + word = word[0] + word[3:] + elif word[3] == "\u0626" and word[2] == "\u0627": # فعائل + word = word[:2] + word[4] + elif word[4] == "\u0629" and word[1] == "\u0627": # فاعلة + word = word[0] + word[2:4] + elif word[4] == "\u064a" and word[2] == "\u0627": # فعالي + word = word[:2] + word[3] + else: + word = self.suf1(word) # do - normalize short sufix + if len(word) == 5: + word = self.pre1(word) # do - normalize short prefix + return word + + def pro_w54(self, word): + """process length five patterns and extract length four roots""" + if word[0] in self.pr53[2]: # تفعلل - افعلل - مفعلل + word = word[1:] + elif word[4] == "\u0629": # فعللة + word = word[:4] + elif word[2] == "\u0627": # فعالل + word = word[:2] + word[3:] + return word + + def end_w5(self, word): + """ending step (word of length five)""" + if len(word) == 4: + word = self.pro_w4(word) + elif len(word) == 5: + word = self.pro_w54(word) + return word + + def pro_w6(self, word): + """process length six patterns and extract length three roots""" + if word.startswith("\u0627\u0633\u062a") or word.startswith( + "\u0645\u0633\u062a" + ): # مستفعل - استفعل + word = word[3:] + elif ( + word[0] == "\u0645" and word[3] == "\u0627" and word[5] == "\u0629" + ): # مفعالة + word = word[1:3] + word[4] + elif ( + word[0] == "\u0627" and word[2] == "\u062a" and word[4] == "\u0627" + ): # افتعال + word = word[1] + word[3] + word[5] + elif ( + word[0] == "\u0627" and word[3] == "\u0648" and word[2] == word[4] + ): # افعوعل + word = word[1] + word[4:] + elif ( + word[0] == "\u062a" and word[2] == "\u0627" and word[4] == "\u064a" + ): # تفاعيل new pattern + word = word[1] + word[3] + word[5] + else: + word = self.suf1(word) # do - normalize short sufix + if len(word) == 6: + word = self.pre1(word) # do - normalize short prefix + return word + + def pro_w64(self, word): + """process length six patterns and extract length four roots""" + if word[0] == "\u0627" and word[4] == "\u0627": # افعلال + word = word[1:4] + word[5] + elif word.startswith("\u0645\u062a"): # متفعلل + word = word[2:] + return word + + def end_w6(self, word): + """ending step (word of length six)""" + if len(word) == 5: + word = self.pro_w53(word) + word = self.end_w5(word) + elif len(word) == 6: + word = self.pro_w64(word) + return word + + def suf1(self, word): + """normalize short sufix""" + for sf1 in self.s1: + if word.endswith(sf1): + return word[:-1] + return word + + def pre1(self, word): + """normalize short prefix""" + for sp1 in self.p1: + if word.startswith(sp1): + return word[1:] + return word diff --git a/nltk/stem/lancaster.py b/nltk/stem/lancaster.py new file mode 100644 index 0000000..db1808d --- /dev/null +++ b/nltk/stem/lancaster.py @@ -0,0 +1,342 @@ +# Natural Language Toolkit: Stemmers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Tomcavage +# URL: +# For license information, see LICENSE.TXT + +""" +A word stemmer based on the Lancaster (Paice/Husk) stemming algorithm. +Paice, Chris D. "Another Stemmer." ACM SIGIR Forum 24.3 (1990): 56-61. +""" +import re + +from nltk.stem.api import StemmerI + + +class LancasterStemmer(StemmerI): + """ + Lancaster Stemmer + + >>> from nltk.stem.lancaster import LancasterStemmer + >>> st = LancasterStemmer() + >>> st.stem('maximum') # Remove "-um" when word is intact + 'maxim' + >>> st.stem('presumably') # Don't remove "-um" when word is not intact + 'presum' + >>> st.stem('multiply') # No action taken if word ends with "-ply" + 'multiply' + >>> st.stem('provision') # Replace "-sion" with "-j" to trigger "j" set of rules + 'provid' + >>> st.stem('owed') # Word starting with vowel must contain at least 2 letters + 'ow' + >>> st.stem('ear') # ditto + 'ear' + >>> st.stem('saying') # Words starting with consonant must contain at least 3 + 'say' + >>> st.stem('crying') # letters and one of those letters must be a vowel + 'cry' + >>> st.stem('string') # ditto + 'string' + >>> st.stem('meant') # ditto + 'meant' + >>> st.stem('cement') # ditto + 'cem' + >>> st_pre = LancasterStemmer(strip_prefix_flag=True) + >>> st_pre.stem('kilometer') # Test Prefix + 'met' + >>> st_custom = LancasterStemmer(rule_tuple=("ssen4>", "s1t.")) + >>> st_custom.stem("ness") # Change s to t + 'nest' + """ + + # The rule list is static since it doesn't change between instances + default_rule_tuple = ( + "ai*2.", # -ia > - if intact + "a*1.", # -a > - if intact + "bb1.", # -bb > -b + "city3s.", # -ytic > -ys + "ci2>", # -ic > - + "cn1t>", # -nc > -nt + "dd1.", # -dd > -d + "dei3y>", # -ied > -y + "deec2ss.", # -ceed >", -cess + "dee1.", # -eed > -ee + "de2>", # -ed > - + "dooh4>", # -hood > - + "e1>", # -e > - + "feil1v.", # -lief > -liev + "fi2>", # -if > - + "gni3>", # -ing > - + "gai3y.", # -iag > -y + "ga2>", # -ag > - + "gg1.", # -gg > -g + "ht*2.", # -th > - if intact + "hsiug5ct.", # -guish > -ct + "hsi3>", # -ish > - + "i*1.", # -i > - if intact + "i1y>", # -i > -y + "ji1d.", # -ij > -id -- see nois4j> & vis3j> + "juf1s.", # -fuj > -fus + "ju1d.", # -uj > -ud + "jo1d.", # -oj > -od + "jeh1r.", # -hej > -her + "jrev1t.", # -verj > -vert + "jsim2t.", # -misj > -mit + "jn1d.", # -nj > -nd + "j1s.", # -j > -s + "lbaifi6.", # -ifiabl > - + "lbai4y.", # -iabl > -y + "lba3>", # -abl > - + "lbi3.", # -ibl > - + "lib2l>", # -bil > -bl + "lc1.", # -cl > c + "lufi4y.", # -iful > -y + "luf3>", # -ful > - + "lu2.", # -ul > - + "lai3>", # -ial > - + "lau3>", # -ual > - + "la2>", # -al > - + "ll1.", # -ll > -l + "mui3.", # -ium > - + "mu*2.", # -um > - if intact + "msi3>", # -ism > - + "mm1.", # -mm > -m + "nois4j>", # -sion > -j + "noix4ct.", # -xion > -ct + "noi3>", # -ion > - + "nai3>", # -ian > - + "na2>", # -an > - + "nee0.", # protect -een + "ne2>", # -en > - + "nn1.", # -nn > -n + "pihs4>", # -ship > - + "pp1.", # -pp > -p + "re2>", # -er > - + "rae0.", # protect -ear + "ra2.", # -ar > - + "ro2>", # -or > - + "ru2>", # -ur > - + "rr1.", # -rr > -r + "rt1>", # -tr > -t + "rei3y>", # -ier > -y + "sei3y>", # -ies > -y + "sis2.", # -sis > -s + "si2>", # -is > - + "ssen4>", # -ness > - + "ss0.", # protect -ss + "suo3>", # -ous > - + "su*2.", # -us > - if intact + "s*1>", # -s > - if intact + "s0.", # -s > -s + "tacilp4y.", # -plicat > -ply + "ta2>", # -at > - + "tnem4>", # -ment > - + "tne3>", # -ent > - + "tna3>", # -ant > - + "tpir2b.", # -ript > -rib + "tpro2b.", # -orpt > -orb + "tcud1.", # -duct > -duc + "tpmus2.", # -sumpt > -sum + "tpec2iv.", # -cept > -ceiv + "tulo2v.", # -olut > -olv + "tsis0.", # protect -sist + "tsi3>", # -ist > - + "tt1.", # -tt > -t + "uqi3.", # -iqu > - + "ugo1.", # -ogu > -og + "vis3j>", # -siv > -j + "vie0.", # protect -eiv + "vi2>", # -iv > - + "ylb1>", # -bly > -bl + "yli3y>", # -ily > -y + "ylp0.", # protect -ply + "yl2>", # -ly > - + "ygo1.", # -ogy > -og + "yhp1.", # -phy > -ph + "ymo1.", # -omy > -om + "ypo1.", # -opy > -op + "yti3>", # -ity > - + "yte3>", # -ety > - + "ytl2.", # -lty > -l + "yrtsi5.", # -istry > - + "yra3>", # -ary > - + "yro3>", # -ory > - + "yfi3.", # -ify > - + "ycn2t>", # -ncy > -nt + "yca3>", # -acy > - + "zi2>", # -iz > - + "zy1s.", # -yz > -ys + ) + + def __init__(self, rule_tuple=None, strip_prefix_flag=False): + """Create an instance of the Lancaster stemmer.""" + # Setup an empty rule dictionary - this will be filled in later + self.rule_dictionary = {} + # Check if a user wants to strip prefix + self._strip_prefix = strip_prefix_flag + # Check if a user wants to use his/her own rule tuples. + self._rule_tuple = rule_tuple if rule_tuple else self.default_rule_tuple + + def parseRules(self, rule_tuple=None): + """Validate the set of rules used in this stemmer. + + If this function is called as an individual method, without using stem + method, rule_tuple argument will be compiled into self.rule_dictionary. + If this function is called within stem, self._rule_tuple will be used. + + """ + # If there is no argument for the function, use class' own rule tuple. + rule_tuple = rule_tuple if rule_tuple else self._rule_tuple + valid_rule = re.compile(r"^[a-z]+\*?\d[a-z]*[>\.]?$") + # Empty any old rules from the rule set before adding new ones + self.rule_dictionary = {} + + for rule in rule_tuple: + if not valid_rule.match(rule): + raise ValueError(f"The rule {rule} is invalid") + first_letter = rule[0:1] + if first_letter in self.rule_dictionary: + self.rule_dictionary[first_letter].append(rule) + else: + self.rule_dictionary[first_letter] = [rule] + + def stem(self, word): + """Stem a word using the Lancaster stemmer.""" + # Lower-case the word, since all the rules are lower-cased + word = word.lower() + word = self.__stripPrefix(word) if self._strip_prefix else word + + # Save a copy of the original word + intact_word = word + + # If rule dictionary is empty, parse rule tuple. + if not self.rule_dictionary: + self.parseRules() + + return self.__doStemming(word, intact_word) + + def __doStemming(self, word, intact_word): + """Perform the actual word stemming""" + + valid_rule = re.compile(r"^([a-z]+)(\*?)(\d)([a-z]*)([>\.]?)$") + + proceed = True + + while proceed: + # Find the position of the last letter of the word to be stemmed + last_letter_position = self.__getLastLetter(word) + + # Only stem the word if it has a last letter and a rule matching that last letter + if ( + last_letter_position < 0 + or word[last_letter_position] not in self.rule_dictionary + ): + proceed = False + + else: + rule_was_applied = False + + # Go through each rule that matches the word's final letter + for rule in self.rule_dictionary[word[last_letter_position]]: + rule_match = valid_rule.match(rule) + if rule_match: + ( + ending_string, + intact_flag, + remove_total, + append_string, + cont_flag, + ) = rule_match.groups() + + # Convert the number of chars to remove when stemming + # from a string to an integer + remove_total = int(remove_total) + + # Proceed if word's ending matches rule's word ending + if word.endswith(ending_string[::-1]): + if intact_flag: + if word == intact_word and self.__isAcceptable( + word, remove_total + ): + word = self.__applyRule( + word, remove_total, append_string + ) + rule_was_applied = True + if cont_flag == ".": + proceed = False + break + elif self.__isAcceptable(word, remove_total): + word = self.__applyRule( + word, remove_total, append_string + ) + rule_was_applied = True + if cont_flag == ".": + proceed = False + break + # If no rules apply, the word doesn't need any more stemming + if not rule_was_applied: + proceed = False + return word + + def __getLastLetter(self, word): + """Get the zero-based index of the last alphabetic character in this string""" + last_letter = -1 + for position in range(len(word)): + if word[position].isalpha(): + last_letter = position + else: + break + return last_letter + + def __isAcceptable(self, word, remove_total): + """Determine if the word is acceptable for stemming.""" + word_is_acceptable = False + # If the word starts with a vowel, it must be at least 2 + # characters long to be stemmed + if word[0] in "aeiouy": + if len(word) - remove_total >= 2: + word_is_acceptable = True + # If the word starts with a consonant, it must be at least 3 + # characters long (including one vowel) to be stemmed + elif len(word) - remove_total >= 3: + if word[1] in "aeiouy": + word_is_acceptable = True + elif word[2] in "aeiouy": + word_is_acceptable = True + return word_is_acceptable + + def __applyRule(self, word, remove_total, append_string): + """Apply the stemming rule to the word""" + # Remove letters from the end of the word + new_word_length = len(word) - remove_total + word = word[0:new_word_length] + + # And add new letters to the end of the truncated word + if append_string: + word += append_string + return word + + def __stripPrefix(self, word): + """Remove prefix from a word. + + This function originally taken from Whoosh. + + """ + for prefix in ( + "kilo", + "micro", + "milli", + "intra", + "ultra", + "mega", + "nano", + "pico", + "pseudo", + ): + if word.startswith(prefix): + return word[len(prefix) :] + return word + + def __repr__(self): + return "" diff --git a/nltk/stem/porter.py b/nltk/stem/porter.py new file mode 100644 index 0000000..73404b8 --- /dev/null +++ b/nltk/stem/porter.py @@ -0,0 +1,724 @@ +""" +Porter Stemmer + +This is the Porter stemming algorithm. It follows the algorithm +presented in + +Porter, M. "An algorithm for suffix stripping." Program 14.3 (1980): 130-137. + +with some optional deviations that can be turned on or off with the +`mode` argument to the constructor. + +Martin Porter, the algorithm's inventor, maintains a web page about the +algorithm at + + https://www.tartarus.org/~martin/PorterStemmer/ + +which includes another Python implementation and other implementations +in many languages. +""" + +__docformat__ = "plaintext" + +import re + +from nltk.stem.api import StemmerI + + +class PorterStemmer(StemmerI): + """ + A word stemmer based on the Porter stemming algorithm. + + Porter, M. "An algorithm for suffix stripping." + Program 14.3 (1980): 130-137. + + See https://www.tartarus.org/~martin/PorterStemmer/ for the homepage + of the algorithm. + + Martin Porter has endorsed several modifications to the Porter + algorithm since writing his original paper, and those extensions are + included in the implementations on his website. Additionally, others + have proposed further improvements to the algorithm, including NLTK + contributors. There are thus three modes that can be selected by + passing the appropriate constant to the class constructor's `mode` + attribute: + + - PorterStemmer.ORIGINAL_ALGORITHM + + An implementation that is faithful to the original paper. + + Note that Martin Porter has deprecated this version of the + algorithm. Martin distributes implementations of the Porter + Stemmer in many languages, hosted at: + + https://www.tartarus.org/~martin/PorterStemmer/ + + and all of these implementations include his extensions. He + strongly recommends against using the original, published + version of the algorithm; only use this mode if you clearly + understand why you are choosing to do so. + + - PorterStemmer.MARTIN_EXTENSIONS + + An implementation that only uses the modifications to the + algorithm that are included in the implementations on Martin + Porter's website. He has declared Porter frozen, so the + behaviour of those implementations should never change. + + - PorterStemmer.NLTK_EXTENSIONS (default) + + An implementation that includes further improvements devised by + NLTK contributors or taken from other modified implementations + found on the web. + + For the best stemming, you should use the default NLTK_EXTENSIONS + version. However, if you need to get the same results as either the + original algorithm or one of Martin Porter's hosted versions for + compatibility with an existing implementation or dataset, you can use + one of the other modes instead. + """ + + # Modes the Stemmer can be instantiated in + NLTK_EXTENSIONS = "NLTK_EXTENSIONS" + MARTIN_EXTENSIONS = "MARTIN_EXTENSIONS" + ORIGINAL_ALGORITHM = "ORIGINAL_ALGORITHM" + + def __init__(self, mode=NLTK_EXTENSIONS): + if mode not in ( + self.NLTK_EXTENSIONS, + self.MARTIN_EXTENSIONS, + self.ORIGINAL_ALGORITHM, + ): + raise ValueError( + "Mode must be one of PorterStemmer.NLTK_EXTENSIONS, " + "PorterStemmer.MARTIN_EXTENSIONS, or " + "PorterStemmer.ORIGINAL_ALGORITHM" + ) + + self.mode = mode + + if self.mode == self.NLTK_EXTENSIONS: + # This is a table of irregular forms. It is quite short, + # but still reflects the errors actually drawn to Martin + # Porter's attention over a 20 year period! + irregular_forms = { + "sky": ["sky", "skies"], + "die": ["dying"], + "lie": ["lying"], + "tie": ["tying"], + "news": ["news"], + "inning": ["innings", "inning"], + "outing": ["outings", "outing"], + "canning": ["cannings", "canning"], + "howe": ["howe"], + "proceed": ["proceed"], + "exceed": ["exceed"], + "succeed": ["succeed"], + } + + self.pool = {} + for key in irregular_forms: + for val in irregular_forms[key]: + self.pool[val] = key + + self.vowels = frozenset(["a", "e", "i", "o", "u"]) + + def _is_consonant(self, word, i): + """Returns True if word[i] is a consonant, False otherwise + + A consonant is defined in the paper as follows: + + A consonant in a word is a letter other than A, E, I, O or + U, and other than Y preceded by a consonant. (The fact that + the term `consonant' is defined to some extent in terms of + itself does not make it ambiguous.) So in TOY the consonants + are T and Y, and in SYZYGY they are S, Z and G. If a letter + is not a consonant it is a vowel. + """ + if word[i] in self.vowels: + return False + if word[i] == "y": + # A 'y' counts as a consonant when the letter before it is not + # one, and as a vowel otherwise. Resolve a run of 'y's + # iteratively instead of recursively so that a token such as + # "yyyy..." cannot drive the recursion depth past the + # interpreter limit and raise an uncaught RecursionError + # (CWE-674). + negate = False + while i > 0 and word[i] == "y": + negate = not negate + i -= 1 + return (word[i] not in self.vowels) != negate + return True + + def _measure(self, stem): + r"""Returns the 'measure' of stem, per definition in the paper + + From the paper: + + A consonant will be denoted by c, a vowel by v. A list + ccc... of length greater than 0 will be denoted by C, and a + list vvv... of length greater than 0 will be denoted by V. + Any word, or part of a word, therefore has one of the four + forms: + + CVCV ... C + CVCV ... V + VCVC ... C + VCVC ... V + + These may all be represented by the single form + + [C]VCVC ... [V] + + where the square brackets denote arbitrary presence of their + contents. Using (VC){m} to denote VC repeated m times, this + may again be written as + + [C](VC){m}[V]. + + m will be called the \measure\ of any word or word part when + represented in this form. The case m = 0 covers the null + word. Here are some examples: + + m=0 TR, EE, TREE, Y, BY. + m=1 TROUBLE, OATS, TREES, IVY. + m=2 TROUBLES, PRIVATE, OATEN, ORRERY. + """ + cv_sequence = "" + + # Construct a string of 'c's and 'v's representing whether each + # character in `stem` is a consonant or a vowel. + # e.g. 'falafel' becomes 'cvcvcvc', + # 'architecture' becomes 'vcccvcvccvcv' + for i in range(len(stem)): + if self._is_consonant(stem, i): + cv_sequence += "c" + else: + cv_sequence += "v" + + # Count the number of 'vc' occurrences, which is equivalent to + # the number of 'VC' occurrences in Porter's reduced form in the + # docstring above, which is in turn equivalent to `m` + return cv_sequence.count("vc") + + def _has_positive_measure(self, stem): + return self._measure(stem) > 0 + + def _contains_vowel(self, stem): + """Returns True if stem contains a vowel, else False""" + for i in range(len(stem)): + if not self._is_consonant(stem, i): + return True + return False + + def _ends_double_consonant(self, word): + """Implements condition *d from the paper + + Returns True if word ends with a double consonant + """ + return ( + len(word) >= 2 + and word[-1] == word[-2] + and self._is_consonant(word, len(word) - 1) + ) + + def _ends_cvc(self, word): + """Implements condition *o from the paper + + From the paper: + + *o - the stem ends cvc, where the second c is not W, X or Y + (e.g. -WIL, -HOP). + """ + return ( + len(word) >= 3 + and self._is_consonant(word, len(word) - 3) + and not self._is_consonant(word, len(word) - 2) + and self._is_consonant(word, len(word) - 1) + and word[-1] not in ("w", "x", "y") + ) or ( + self.mode == self.NLTK_EXTENSIONS + and len(word) == 2 + and not self._is_consonant(word, 0) + and self._is_consonant(word, 1) + ) + + def _replace_suffix(self, word, suffix, replacement): + """Replaces `suffix` of `word` with `replacement""" + assert word.endswith(suffix), "Given word doesn't end with given suffix" + if suffix == "": + return word + replacement + else: + return word[: -len(suffix)] + replacement + + def _apply_rule_list(self, word, rules): + """Applies the first applicable suffix-removal rule to the word + + Takes a word and a list of suffix-removal rules represented as + 3-tuples, with the first element being the suffix to remove, + the second element being the string to replace it with, and the + final element being the condition for the rule to be applicable, + or None if the rule is unconditional. + """ + for rule in rules: + suffix, replacement, condition = rule + if suffix == "*d" and self._ends_double_consonant(word): + stem = word[:-2] + if condition is None or condition(stem): + return stem + replacement + else: + # Don't try any further rules + return word + if word.endswith(suffix): + stem = self._replace_suffix(word, suffix, "") + if condition is None or condition(stem): + return stem + replacement + else: + # Don't try any further rules + return word + + return word + + def _step1a(self, word): + """Implements Step 1a from "An algorithm for suffix stripping" + + From the paper: + + SSES -> SS caresses -> caress + IES -> I ponies -> poni + ties -> ti + SS -> SS caress -> caress + S -> cats -> cat + """ + # this NLTK-only rule extends the original algorithm, so + # that 'flies'->'fli' but 'dies'->'die' etc + if self.mode == self.NLTK_EXTENSIONS: + if word.endswith("ies") and len(word) == 4: + return self._replace_suffix(word, "ies", "ie") + + return self._apply_rule_list( + word, + [ + ("sses", "ss", None), # SSES -> SS + ("ies", "i", None), # IES -> I + ("ss", "ss", None), # SS -> SS + ("s", "", None), # S -> + ], + ) + + def _step1b(self, word): + """Implements Step 1b from "An algorithm for suffix stripping" + + From the paper: + + (m>0) EED -> EE feed -> feed + agreed -> agree + (*v*) ED -> plastered -> plaster + bled -> bled + (*v*) ING -> motoring -> motor + sing -> sing + + If the second or third of the rules in Step 1b is successful, + the following is done: + + AT -> ATE conflat(ed) -> conflate + BL -> BLE troubl(ed) -> trouble + IZ -> IZE siz(ed) -> size + (*d and not (*L or *S or *Z)) + -> single letter + hopp(ing) -> hop + tann(ed) -> tan + fall(ing) -> fall + hiss(ing) -> hiss + fizz(ed) -> fizz + (m=1 and *o) -> E fail(ing) -> fail + fil(ing) -> file + + The rule to map to a single letter causes the removal of one of + the double letter pair. The -E is put back on -AT, -BL and -IZ, + so that the suffixes -ATE, -BLE and -IZE can be recognised + later. This E may be removed in step 4. + """ + # this NLTK-only block extends the original algorithm, so that + # 'spied'->'spi' but 'died'->'die' etc + if self.mode == self.NLTK_EXTENSIONS: + if word.endswith("ied"): + if len(word) == 4: + return self._replace_suffix(word, "ied", "ie") + else: + return self._replace_suffix(word, "ied", "i") + + # (m>0) EED -> EE + if word.endswith("eed"): + stem = self._replace_suffix(word, "eed", "") + if self._measure(stem) > 0: + return stem + "ee" + else: + return word + + rule_2_or_3_succeeded = False + + for suffix in ["ed", "ing"]: + if word.endswith(suffix): + intermediate_stem = self._replace_suffix(word, suffix, "") + if self._contains_vowel(intermediate_stem): + rule_2_or_3_succeeded = True + break + + if not rule_2_or_3_succeeded: + return word + + return self._apply_rule_list( + intermediate_stem, + [ + ("at", "ate", None), # AT -> ATE + ("bl", "ble", None), # BL -> BLE + ("iz", "ize", None), # IZ -> IZE + # (*d and not (*L or *S or *Z)) + # -> single letter + ( + "*d", + intermediate_stem[-1], + lambda stem: intermediate_stem[-1] not in ("l", "s", "z"), + ), + # (m=1 and *o) -> E + ( + "", + "e", + lambda stem: (self._measure(stem) == 1 and self._ends_cvc(stem)), + ), + ], + ) + + def _step1c(self, word): + """Implements Step 1c from "An algorithm for suffix stripping" + + From the paper: + + Step 1c + + (*v*) Y -> I happy -> happi + sky -> sky + """ + + def nltk_condition(stem): + """ + This has been modified from the original Porter algorithm so + that y->i is only done when y is preceded by a consonant, + but not if the stem is only a single consonant, i.e. + + (*c and not c) Y -> I + + So 'happy' -> 'happi', but + 'enjoy' -> 'enjoy' etc + + This is a much better rule. Formerly 'enjoy'->'enjoi' and + 'enjoyment'->'enjoy'. Step 1c is perhaps done too soon; but + with this modification that no longer really matters. + + Also, the removal of the contains_vowel(z) condition means + that 'spy', 'fly', 'try' ... stem to 'spi', 'fli', 'tri' and + conflate with 'spied', 'tried', 'flies' ... + """ + return len(stem) > 1 and self._is_consonant(stem, len(stem) - 1) + + def original_condition(stem): + return self._contains_vowel(stem) + + return self._apply_rule_list( + word, + [ + ( + "y", + "i", + ( + nltk_condition + if self.mode == self.NLTK_EXTENSIONS + else original_condition + ), + ) + ], + ) + + def _step2(self, word): + """Implements Step 2 from "An algorithm for suffix stripping" + + From the paper: + + Step 2 + + (m>0) ATIONAL -> ATE relational -> relate + (m>0) TIONAL -> TION conditional -> condition + rational -> rational + (m>0) ENCI -> ENCE valenci -> valence + (m>0) ANCI -> ANCE hesitanci -> hesitance + (m>0) IZER -> IZE digitizer -> digitize + (m>0) ABLI -> ABLE conformabli -> conformable + (m>0) ALLI -> AL radicalli -> radical + (m>0) ENTLI -> ENT differentli -> different + (m>0) ELI -> E vileli - > vile + (m>0) OUSLI -> OUS analogousli -> analogous + (m>0) IZATION -> IZE vietnamization -> vietnamize + (m>0) ATION -> ATE predication -> predicate + (m>0) ATOR -> ATE operator -> operate + (m>0) ALISM -> AL feudalism -> feudal + (m>0) IVENESS -> IVE decisiveness -> decisive + (m>0) FULNESS -> FUL hopefulness -> hopeful + (m>0) OUSNESS -> OUS callousness -> callous + (m>0) ALITI -> AL formaliti -> formal + (m>0) IVITI -> IVE sensitiviti -> sensitive + (m>0) BILITI -> BLE sensibiliti -> sensible + """ + + if self.mode == self.NLTK_EXTENSIONS: + # Instead of applying the ALLI -> AL rule after '(a)bli' per + # the published algorithm, instead we apply it first, and, + # if it succeeds, run the result through step2 again. + if word.endswith("alli") and self._has_positive_measure( + self._replace_suffix(word, "alli", "") + ): + return self._step2(self._replace_suffix(word, "alli", "al")) + + bli_rule = ("bli", "ble", self._has_positive_measure) + abli_rule = ("abli", "able", self._has_positive_measure) + + rules = [ + ("ational", "ate", self._has_positive_measure), + ("tional", "tion", self._has_positive_measure), + ("enci", "ence", self._has_positive_measure), + ("anci", "ance", self._has_positive_measure), + ("izer", "ize", self._has_positive_measure), + abli_rule if self.mode == self.ORIGINAL_ALGORITHM else bli_rule, + ("alli", "al", self._has_positive_measure), + ("entli", "ent", self._has_positive_measure), + ("eli", "e", self._has_positive_measure), + ("ousli", "ous", self._has_positive_measure), + ("ization", "ize", self._has_positive_measure), + ("ation", "ate", self._has_positive_measure), + ("ator", "ate", self._has_positive_measure), + ("alism", "al", self._has_positive_measure), + ("iveness", "ive", self._has_positive_measure), + ("fulness", "ful", self._has_positive_measure), + ("ousness", "ous", self._has_positive_measure), + ("aliti", "al", self._has_positive_measure), + ("iviti", "ive", self._has_positive_measure), + ("biliti", "ble", self._has_positive_measure), + ] + + if self.mode == self.NLTK_EXTENSIONS: + rules.append(("fulli", "ful", self._has_positive_measure)) + + # The 'l' of the 'logi' -> 'log' rule is put with the stem, + # so that short stems like 'geo' 'theo' etc work like + # 'archaeo' 'philo' etc. + rules.append( + ("logi", "log", lambda stem: self._has_positive_measure(word[:-3])) + ) + + if self.mode == self.MARTIN_EXTENSIONS: + rules.append(("logi", "log", self._has_positive_measure)) + + return self._apply_rule_list(word, rules) + + def _step3(self, word): + """Implements Step 3 from "An algorithm for suffix stripping" + + From the paper: + + Step 3 + + (m>0) ICATE -> IC triplicate -> triplic + (m>0) ATIVE -> formative -> form + (m>0) ALIZE -> AL formalize -> formal + (m>0) ICITI -> IC electriciti -> electric + (m>0) ICAL -> IC electrical -> electric + (m>0) FUL -> hopeful -> hope + (m>0) NESS -> goodness -> good + """ + return self._apply_rule_list( + word, + [ + ("icate", "ic", self._has_positive_measure), + ("ative", "", self._has_positive_measure), + ("alize", "al", self._has_positive_measure), + ("iciti", "ic", self._has_positive_measure), + ("ical", "ic", self._has_positive_measure), + ("ful", "", self._has_positive_measure), + ("ness", "", self._has_positive_measure), + ], + ) + + def _step4(self, word): + """Implements Step 4 from "An algorithm for suffix stripping" + + Step 4 + + (m>1) AL -> revival -> reviv + (m>1) ANCE -> allowance -> allow + (m>1) ENCE -> inference -> infer + (m>1) ER -> airliner -> airlin + (m>1) IC -> gyroscopic -> gyroscop + (m>1) ABLE -> adjustable -> adjust + (m>1) IBLE -> defensible -> defens + (m>1) ANT -> irritant -> irrit + (m>1) EMENT -> replacement -> replac + (m>1) MENT -> adjustment -> adjust + (m>1) ENT -> dependent -> depend + (m>1 and (*S or *T)) ION -> adoption -> adopt + (m>1) OU -> homologou -> homolog + (m>1) ISM -> communism -> commun + (m>1) ATE -> activate -> activ + (m>1) ITI -> angulariti -> angular + (m>1) OUS -> homologous -> homolog + (m>1) IVE -> effective -> effect + (m>1) IZE -> bowdlerize -> bowdler + + The suffixes are now removed. All that remains is a little + tidying up. + """ + measure_gt_1 = lambda stem: self._measure(stem) > 1 + + return self._apply_rule_list( + word, + [ + ("al", "", measure_gt_1), + ("ance", "", measure_gt_1), + ("ence", "", measure_gt_1), + ("er", "", measure_gt_1), + ("ic", "", measure_gt_1), + ("able", "", measure_gt_1), + ("ible", "", measure_gt_1), + ("ant", "", measure_gt_1), + ("ement", "", measure_gt_1), + ("ment", "", measure_gt_1), + ("ent", "", measure_gt_1), + # (m>1 and (*S or *T)) ION -> + ( + "ion", + "", + lambda stem: self._measure(stem) > 1 and stem[-1] in ("s", "t"), + ), + ("ou", "", measure_gt_1), + ("ism", "", measure_gt_1), + ("ate", "", measure_gt_1), + ("iti", "", measure_gt_1), + ("ous", "", measure_gt_1), + ("ive", "", measure_gt_1), + ("ize", "", measure_gt_1), + ], + ) + + def _step5a(self, word): + """Implements Step 5a from "An algorithm for suffix stripping" + + From the paper: + + Step 5a + + (m>1) E -> probate -> probat + rate -> rate + (m=1 and not *o) E -> cease -> ceas + """ + # Note that Martin's test vocabulary and reference + # implementations are inconsistent in how they handle the case + # where two rules both refer to a suffix that matches the word + # to be stemmed, but only the condition of the second one is + # true. + # Earlier in step2b we had the rules: + # (m>0) EED -> EE + # (*v*) ED -> + # but the examples in the paper included "feed"->"feed", even + # though (*v*) is true for "fe" and therefore the second rule + # alone would map "feed"->"fe". + # However, in THIS case, we need to handle the consecutive rules + # differently and try both conditions (obviously; the second + # rule here would be redundant otherwise). Martin's paper makes + # no explicit mention of the inconsistency; you have to infer it + # from the examples. + # For this reason, we can't use _apply_rule_list here. + if word.endswith("e"): + stem = self._replace_suffix(word, "e", "") + if self._measure(stem) > 1: + return stem + if self._measure(stem) == 1 and not self._ends_cvc(stem): + return stem + return word + + def _step5b(self, word): + """Implements Step 5a from "An algorithm for suffix stripping" + + From the paper: + + Step 5b + + (m > 1 and *d and *L) -> single letter + controll -> control + roll -> roll + """ + return self._apply_rule_list( + word, [("ll", "l", lambda stem: self._measure(word[:-1]) > 1)] + ) + + def stem(self, word, to_lowercase=True): + """ + :param to_lowercase: if `to_lowercase=True` the word always lowercase + """ + stem = word.lower() if to_lowercase else word + + if self.mode == self.NLTK_EXTENSIONS and stem in self.pool: + return self.pool[stem] + + if self.mode != self.ORIGINAL_ALGORITHM and len(word) <= 2: + # With this line, strings of length 1 or 2 don't go through + # the stemming process, although no mention is made of this + # in the published algorithm. + return stem + + stem = self._step1a(stem) + stem = self._step1b(stem) + stem = self._step1c(stem) + stem = self._step2(stem) + stem = self._step3(stem) + stem = self._step4(stem) + stem = self._step5a(stem) + stem = self._step5b(stem) + + return stem + + def __repr__(self): + return "" + + +def demo(): + """ + A demonstration of the porter stemmer on a sample from + the Penn Treebank corpus. + """ + + from nltk import stem + from nltk.corpus import treebank + + stemmer = stem.PorterStemmer() + + orig = [] + stemmed = [] + for item in treebank.fileids()[:3]: + for word, tag in treebank.tagged_words(item): + orig.append(word) + stemmed.append(stemmer.stem(word)) + + # Convert the results to a string, and word-wrap them. + results = " ".join(stemmed) + results = re.sub(r"(.{,70})\s", r"\1\n", results + " ").rstrip() + + # Convert the original to a string, and word wrap it. + original = " ".join(orig) + original = re.sub(r"(.{,70})\s", r"\1\n", original + " ").rstrip() + + # Print the results. + print("-Original-".center(70).replace(" ", "*").replace("-", " ")) + print(original) + print("-Results-".center(70).replace(" ", "*").replace("-", " ")) + print(results) + print("*" * 70) diff --git a/nltk/stem/regexp.py b/nltk/stem/regexp.py new file mode 100644 index 0000000..06dd0af --- /dev/null +++ b/nltk/stem/regexp.py @@ -0,0 +1,55 @@ +# Natural Language Toolkit: Stemmers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT +import re + +from nltk.stem.api import StemmerI + + +class RegexpStemmer(StemmerI): + """ + A stemmer that uses regular expressions to identify morphological + affixes. Any substrings that match the regular expressions will + be removed. + + >>> from nltk.stem import RegexpStemmer + >>> st = RegexpStemmer('ing$|s$|e$|able$', min=4) + >>> st.stem('cars') + 'car' + >>> st.stem('mass') + 'mas' + >>> st.stem('was') + 'was' + >>> st.stem('bee') + 'bee' + >>> st.stem('compute') + 'comput' + >>> st.stem('advisable') + 'advis' + + :type regexp: str or regexp + :param regexp: The regular expression that should be used to + identify morphological affixes. + :type min: int + :param min: The minimum length of string to stem + """ + + def __init__(self, regexp, min=0): + if not hasattr(regexp, "pattern"): + regexp = re.compile(regexp) + self._regexp = regexp + self._min = min + + def stem(self, word): + if len(word) < self._min: + return word + else: + return self._regexp.sub("", word) + + def __repr__(self): + return f"" diff --git a/nltk/stem/rslp.py b/nltk/stem/rslp.py new file mode 100644 index 0000000..99b7094 --- /dev/null +++ b/nltk/stem/rslp.py @@ -0,0 +1,137 @@ +# Natural Language Toolkit: RSLP Stemmer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Tiago Tresoldi +# URL: +# For license information, see LICENSE.TXT + +# This code is based on the algorithm presented in the paper "A Stemming +# Algorithm for the Portuguese Language" by Viviane Moreira Orengo and +# Christian Huyck, which unfortunately I had no access to. The code is a +# Python version, with some minor modifications of mine, to the description +# presented at https://www.webcitation.org/5NnvdIzOb and to the C source code +# available at http://www.inf.ufrgs.br/~arcoelho/rslp/integrando_rslp.html. +# Please note that this stemmer is intended for demonstration and educational +# purposes only. Feel free to write me for any comments, including the +# development of a different and/or better stemmer for Portuguese. I also +# suggest using NLTK's mailing list for Portuguese for any discussion. + +# Este código é baseado no algoritmo apresentado no artigo "A Stemming +# Algorithm for the Portuguese Language" de Viviane Moreira Orengo e +# Christian Huyck, o qual infelizmente não tive a oportunidade de ler. O +# código é uma conversão para Python, com algumas pequenas modificações +# minhas, daquele apresentado em https://www.webcitation.org/5NnvdIzOb e do +# código para linguagem C disponível em +# http://www.inf.ufrgs.br/~arcoelho/rslp/integrando_rslp.html. Por favor, +# lembre-se de que este stemmer foi desenvolvido com finalidades unicamente +# de demonstração e didáticas. Sinta-se livre para me escrever para qualquer +# comentário, inclusive sobre o desenvolvimento de um stemmer diferente +# e/ou melhor para o português. Também sugiro utilizar-se a lista de discussão +# do NLTK para o português para qualquer debate. + +from nltk.data import load +from nltk.stem.api import StemmerI + + +class RSLPStemmer(StemmerI): + """ + A stemmer for Portuguese. + + >>> from nltk.stem import RSLPStemmer + >>> st = RSLPStemmer() + >>> # opening lines of Erico Verissimo's "Música ao Longe" + >>> text = ''' + ... Clarissa risca com giz no quadro-negro a paisagem que os alunos + ... devem copiar . Uma casinha de porta e janela , em cima duma + ... coxilha .''' + >>> for token in text.split(): # doctest: +NORMALIZE_WHITESPACE + ... print(st.stem(token)) + clariss risc com giz no quadro-negr a pais que os alun dev copi . + uma cas de port e janel , em cim dum coxilh . + """ + + def __init__(self): + self._model = [] + + self._model.append(self.read_rule("step0.pt")) + self._model.append(self.read_rule("step1.pt")) + self._model.append(self.read_rule("step2.pt")) + self._model.append(self.read_rule("step3.pt")) + self._model.append(self.read_rule("step4.pt")) + self._model.append(self.read_rule("step5.pt")) + self._model.append(self.read_rule("step6.pt")) + + def read_rule(self, filename): + rules = load("nltk:stemmers/rslp/" + filename, format="raw").decode("utf8") + lines = rules.split("\n") + + lines = [line for line in lines if line != ""] # remove blank lines + lines = [line for line in lines if line[0] != "#"] # remove comments + + # NOTE: a simple but ugly hack to make this parser happy with double '\t's + lines = [line.replace("\t\t", "\t") for line in lines] + + # parse rules + rules = [] + for line in lines: + rule = [] + tokens = line.split("\t") + + # text to be searched for at the end of the string + rule.append(tokens[0][1:-1]) # remove quotes + + # minimum stem size to perform the replacement + rule.append(int(tokens[1])) + + # text to be replaced into + rule.append(tokens[2][1:-1]) # remove quotes + + # exceptions to this rule + rule.append([token[1:-1] for token in tokens[3].split(",")]) + + # append to the results + rules.append(rule) + + return rules + + def stem(self, word): + word = word.lower() + + # the word ends in 's'? apply rule for plural reduction + if word[-1] == "s": + word = self.apply_rule(word, 0) + + # the word ends in 'a'? apply rule for feminine reduction + if word[-1] == "a": + word = self.apply_rule(word, 1) + + # augmentative reduction + word = self.apply_rule(word, 3) + + # adverb reduction + word = self.apply_rule(word, 2) + + # noun reduction + prev_word = word + word = self.apply_rule(word, 4) + if word == prev_word: + # verb reduction + prev_word = word + word = self.apply_rule(word, 5) + if word == prev_word: + # vowel removal + word = self.apply_rule(word, 6) + + return word + + def apply_rule(self, word, rule_index): + rules = self._model[rule_index] + for rule in rules: + suffix_length = len(rule[0]) + if word[-suffix_length:] == rule[0]: # if suffix matches + if len(word) >= suffix_length + rule[1]: # if we have minimum size + if word not in rule[3]: # if not an exception + word = word[:-suffix_length] + rule[2] + break + + return word diff --git a/nltk/stem/snowball.py b/nltk/stem/snowball.py new file mode 100644 index 0000000..b4972bc --- /dev/null +++ b/nltk/stem/snowball.py @@ -0,0 +1,5921 @@ +# +# Natural Language Toolkit: Snowball Stemmer +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Peter Michael Stahl +# Peter Ljunglof (revisions) +# Lakhdar Benzahia (co-writer) +# Assem Chelli (reviewer arabicstemmer) +# Abdelkrim Aries (reviewer arabicstemmer) +# Algorithms: Dr Martin Porter +# Assem Chelli arabic stemming algorithm +# Benzahia Lakhdar +# URL: +# For license information, see LICENSE.TXT + +""" +Snowball stemmers + +This module provides a port of the Snowball stemmers +developed by Martin Porter. + +There is also a demo function: `snowball.demo()`. + +""" + +import re + +from nltk.corpus import stopwords +from nltk.stem import porter +from nltk.stem.api import StemmerI +from nltk.stem.util import prefix_replace, suffix_replace + + +class SnowballStemmer(StemmerI): + """ + Snowball Stemmer + + The following languages are supported: + Arabic, Danish, Dutch, English, Finnish, French, German, + Hungarian, Italian, Norwegian, Portuguese, Romanian, Russian, + Spanish and Swedish. + + The algorithm for English is documented here: + + Porter, M. \"An algorithm for suffix stripping.\" + Program 14.3 (1980): 130-137. + + The algorithms have been developed by Martin Porter. + These stemmers are called Snowball, because Porter created + a programming language with this name for creating + new stemming algorithms. There is more information available + at http://snowball.tartarus.org/ + + The stemmer is invoked as shown below: + + >>> from nltk.stem import SnowballStemmer # See which languages are supported + >>> print(" ".join(SnowballStemmer.languages)) # doctest: +NORMALIZE_WHITESPACE + arabic danish dutch english finnish french german hungarian + italian norwegian porter portuguese romanian russian + spanish swedish + >>> stemmer = SnowballStemmer("german") # Choose a language + >>> stemmer.stem("Autobahnen") # Stem a word + 'autobahn' + + Invoking the stemmers that way is useful if you do not know the + language to be stemmed at runtime. Alternatively, if you already know + the language, then you can invoke the language specific stemmer directly: + + >>> from nltk.stem.snowball import GermanStemmer + >>> stemmer = GermanStemmer() + >>> stemmer.stem("Autobahnen") + 'autobahn' + + :param language: The language whose subclass is instantiated. + :type language: str or unicode + :param ignore_stopwords: If set to True, stopwords are + not stemmed and returned unchanged. + Set to False by default. + :type ignore_stopwords: bool + :raise ValueError: If there is no stemmer for the specified + language, a ValueError is raised. + """ + + languages = ( + "arabic", + "danish", + "dutch", + "english", + "finnish", + "french", + "german", + "hungarian", + "italian", + "norwegian", + "porter", + "portuguese", + "romanian", + "russian", + "spanish", + "swedish", + ) + + def __init__(self, language, ignore_stopwords=False): + if language not in self.languages: + raise ValueError(f"The language '{language}' is not supported.") + stemmerclass = globals()[language.capitalize() + "Stemmer"] + self.stemmer = stemmerclass(ignore_stopwords) + self.stem = self.stemmer.stem + self.stopwords = self.stemmer.stopwords + + def stem(self, token): + return self.stemmer.stem(self, token) + + +class _LanguageSpecificStemmer(StemmerI): + """ + This helper subclass offers the possibility + to invoke a specific stemmer directly. + This is useful if you already know the language to be stemmed at runtime. + + Create an instance of the Snowball stemmer. + + :param ignore_stopwords: If set to True, stopwords are + not stemmed and returned unchanged. + Set to False by default. + :type ignore_stopwords: bool + """ + + def __init__(self, ignore_stopwords=False): + # The language is the name of the class, minus the final "Stemmer". + language = type(self).__name__.lower() + if language.endswith("stemmer"): + language = language[:-7] + + self.stopwords = set() + if ignore_stopwords: + try: + for word in stopwords.words(language): + self.stopwords.add(word) + except OSError as e: + raise ValueError( + "{!r} has no list of stopwords. Please set" + " 'ignore_stopwords' to 'False'.".format(self) + ) from e + + def __repr__(self): + """ + Print out the string representation of the respective class. + + """ + return f"<{type(self).__name__}>" + + +class PorterStemmer(_LanguageSpecificStemmer, porter.PorterStemmer): + """ + A word stemmer based on the original Porter stemming algorithm. + + Porter, M. \"An algorithm for suffix stripping.\" + Program 14.3 (1980): 130-137. + + A few minor modifications have been made to Porter's basic + algorithm. See the source code of the module + nltk.stem.porter for more information. + + """ + + def __init__(self, ignore_stopwords=False): + _LanguageSpecificStemmer.__init__(self, ignore_stopwords) + porter.PorterStemmer.__init__(self) + + +class _ScandinavianStemmer(_LanguageSpecificStemmer): + """ + This subclass encapsulates a method for defining the string region R1. + It is used by the Danish, Norwegian, and Swedish stemmer. + + """ + + def _r1_scandinavian(self, word, vowels): + """ + Return the region R1 that is used by the Scandinavian stemmers. + + R1 is the region after the first non-vowel following a vowel, + or is the null region at the end of the word if there is no + such non-vowel. But then R1 is adjusted so that the region + before it contains at least three letters. + + :param word: The word whose region R1 is determined. + :type word: str or unicode + :param vowels: The vowels of the respective language that are + used to determine the region R1. + :type vowels: unicode + :return: the region R1 for the respective word. + :rtype: unicode + :note: This helper method is invoked by the respective stem method of + the subclasses DanishStemmer, NorwegianStemmer, and + SwedishStemmer. It is not to be invoked directly! + + """ + r1 = "" + for i in range(1, len(word)): + if word[i] not in vowels and word[i - 1] in vowels: + if 3 > len(word[: i + 1]) > 0: + r1 = word[3:] + elif len(word[: i + 1]) >= 3: + r1 = word[i + 1 :] + else: + return word + break + + return r1 + + +class _StandardStemmer(_LanguageSpecificStemmer): + """ + This subclass encapsulates two methods for defining the standard versions + of the string regions R1, R2, and RV. + + """ + + def _r1r2_standard(self, word, vowels): + """ + Return the standard interpretations of the string regions R1 and R2. + + R1 is the region after the first non-vowel following a vowel, + or is the null region at the end of the word if there is no + such non-vowel. + + R2 is the region after the first non-vowel following a vowel + in R1, or is the null region at the end of the word if there + is no such non-vowel. + + :param word: The word whose regions R1 and R2 are determined. + :type word: str or unicode + :param vowels: The vowels of the respective language that are + used to determine the regions R1 and R2. + :type vowels: unicode + :return: (r1,r2), the regions R1 and R2 for the respective word. + :rtype: tuple + :note: This helper method is invoked by the respective stem method of + the subclasses DutchStemmer, FinnishStemmer, + FrenchStemmer, GermanStemmer, ItalianStemmer, + PortugueseStemmer, RomanianStemmer, and SpanishStemmer. + It is not to be invoked directly! + :note: A detailed description of how to define R1 and R2 + can be found at http://snowball.tartarus.org/texts/r1r2.html + + """ + r1 = "" + r2 = "" + for i in range(1, len(word)): + if word[i] not in vowels and word[i - 1] in vowels: + r1 = word[i + 1 :] + break + + for i in range(1, len(r1)): + if r1[i] not in vowels and r1[i - 1] in vowels: + r2 = r1[i + 1 :] + break + + return (r1, r2) + + def _rv_standard(self, word, vowels): + """ + Return the standard interpretation of the string region RV. + + If the second letter is a consonant, RV is the region after the + next following vowel. If the first two letters are vowels, RV is + the region after the next following consonant. Otherwise, RV is + the region after the third letter. + + :param word: The word whose region RV is determined. + :type word: str or unicode + :param vowels: The vowels of the respective language that are + used to determine the region RV. + :type vowels: unicode + :return: the region RV for the respective word. + :rtype: unicode + :note: This helper method is invoked by the respective stem method of + the subclasses ItalianStemmer, PortugueseStemmer, + RomanianStemmer, and SpanishStemmer. It is not to be + invoked directly! + + """ + rv = "" + if len(word) >= 2: + if word[1] not in vowels: + for i in range(2, len(word)): + if word[i] in vowels: + rv = word[i + 1 :] + break + + elif word[0] in vowels and word[1] in vowels: + for i in range(2, len(word)): + if word[i] not in vowels: + rv = word[i + 1 :] + break + else: + rv = word[3:] + + return rv + + +class ArabicStemmer(_StandardStemmer): + """ + https://github.com/snowballstem/snowball/blob/master/algorithms/arabic/stem_Unicode.sbl (Original Algorithm) + The Snowball Arabic light Stemmer + Algorithm: + + - Assem Chelli + - Abdelkrim Aries + - Lakhdar Benzahia + + NLTK Version Author: + + - Lakhdar Benzahia + """ + + # Normalize_pre stes + __vocalization = re.compile( + r"[\u064b-\u064c-\u064d-\u064e-\u064f-\u0650-\u0651-\u0652]" + ) # ً، ٌ، ٍ، َ، ُ، ِ، ّ، ْ + + __kasheeda = re.compile(r"[\u0640]") # ـ tatweel/kasheeda + + __arabic_punctuation_marks = re.compile(r"[\u060C-\u061B-\u061F]") # ؛ ، ؟ + + # Normalize_post + __last_hamzat = ("\u0623", "\u0625", "\u0622", "\u0624", "\u0626") # أ، إ، آ، ؤ، ئ + + # normalize other hamza's + __initial_hamzat = re.compile(r"^[\u0622\u0623\u0625]") # أ، إ، آ + + __waw_hamza = re.compile(r"[\u0624]") # ؤ + + __yeh_hamza = re.compile(r"[\u0626]") # ئ + + __alefat = re.compile(r"[\u0623\u0622\u0625]") # أ، إ، آ + + # Checks + __checks1 = ( + "\u0643\u0627\u0644", + "\u0628\u0627\u0644", # بال، كال + "\u0627\u0644", + "\u0644\u0644", # لل، ال + ) + + __checks2 = ("\u0629", "\u0627\u062a") # ة # female plural ات + + # Suffixes + __suffix_noun_step1a = ( + "\u064a", + "\u0643", + "\u0647", # ي، ك، ه + "\u0646\u0627", + "\u0643\u0645", + "\u0647\u0627", + "\u0647\u0646", + "\u0647\u0645", # نا، كم، ها، هن، هم + "\u0643\u0645\u0627", + "\u0647\u0645\u0627", # كما، هما + ) + + __suffix_noun_step1b = "\u0646" # ن + + __suffix_noun_step2a = ("\u0627", "\u064a", "\u0648") # ا، ي، و + + __suffix_noun_step2b = "\u0627\u062a" # ات + + __suffix_noun_step2c1 = "\u062a" # ت + + __suffix_noun_step2c2 = "\u0629" # ة + + __suffix_noun_step3 = "\u064a" # ي + + __suffix_verb_step1 = ( + "\u0647", + "\u0643", # ه، ك + "\u0646\u064a", + "\u0646\u0627", + "\u0647\u0627", + "\u0647\u0645", # ني، نا، ها، هم + "\u0647\u0646", + "\u0643\u0645", + "\u0643\u0646", # هن، كم، كن + "\u0647\u0645\u0627", + "\u0643\u0645\u0627", + "\u0643\u0645\u0648", # هما، كما، كمو + ) + + __suffix_verb_step2a = ( + "\u062a", + "\u0627", + "\u0646", + "\u064a", # ت، ا، ن، ي + "\u0646\u0627", + "\u062a\u0627", + "\u062a\u0646", # نا، تا، تن Past + "\u0627\u0646", + "\u0648\u0646", + "\u064a\u0646", # ان، هن، ين Present + "\u062a\u0645\u0627", # تما + ) + + __suffix_verb_step2b = ("\u0648\u0627", "\u062a\u0645") # وا، تم + + __suffix_verb_step2c = ("\u0648", "\u062a\u0645\u0648") # و # تمو + + __suffix_all_alef_maqsura = "\u0649" # ى + + # Prefixes + __prefix_step1 = ( + "\u0623", # أ + "\u0623\u0623", + "\u0623\u0622", + "\u0623\u0624", + "\u0623\u0627", + "\u0623\u0625", # أأ، أآ، أؤ، أا، أإ + ) + + __prefix_step2a = ("\u0641\u0627\u0644", "\u0648\u0627\u0644") # فال، وال + + __prefix_step2b = ("\u0641", "\u0648") # ف، و + + __prefix_step3a_noun = ( + "\u0627\u0644", + "\u0644\u0644", # لل، ال + "\u0643\u0627\u0644", + "\u0628\u0627\u0644", # بال، كال + ) + + __prefix_step3b_noun = ( + "\u0628", + "\u0643", + "\u0644", # ب، ك، ل + "\u0628\u0628", + "\u0643\u0643", # بب، كك + ) + + __prefix_step3_verb = ( + "\u0633\u064a", + "\u0633\u062a", + "\u0633\u0646", + "\u0633\u0623", + ) # سي، ست، سن، سأ + + __prefix_step4_verb = ( + "\u064a\u0633\u062a", + "\u0646\u0633\u062a", + "\u062a\u0633\u062a", + ) # يست، نست، تست + + # Suffixes added due to Conjugation Verbs + __conjugation_suffix_verb_1 = ("\u0647", "\u0643") # ه، ك + + __conjugation_suffix_verb_2 = ( + "\u0646\u064a", + "\u0646\u0627", + "\u0647\u0627", # ني، نا، ها + "\u0647\u0645", + "\u0647\u0646", + "\u0643\u0645", # هم، هن، كم + "\u0643\u0646", # كن + ) + __conjugation_suffix_verb_3 = ( + "\u0647\u0645\u0627", + "\u0643\u0645\u0627", + "\u0643\u0645\u0648", + ) # هما، كما، كمو + + __conjugation_suffix_verb_4 = ("\u0627", "\u0646", "\u064a") # ا، ن، ي + + __conjugation_suffix_verb_past = ( + "\u0646\u0627", + "\u062a\u0627", + "\u062a\u0646", + ) # نا، تا، تن + + __conjugation_suffix_verb_present = ( + "\u0627\u0646", + "\u0648\u0646", + "\u064a\u0646", + ) # ان، ون، ين + + # Suffixes added due to derivation Names + __conjugation_suffix_noun_1 = ("\u064a", "\u0643", "\u0647") # ي، ك، ه + + __conjugation_suffix_noun_2 = ( + "\u0646\u0627", + "\u0643\u0645", # نا، كم + "\u0647\u0627", + "\u0647\u0646", + "\u0647\u0645", # ها، هن، هم + ) + + __conjugation_suffix_noun_3 = ( + "\u0643\u0645\u0627", + "\u0647\u0645\u0627", + ) # كما، هما + + # Prefixes added due to derivation Names + __prefixes1 = ("\u0648\u0627", "\u0641\u0627") # فا، وا + + __articles_3len = ("\u0643\u0627\u0644", "\u0628\u0627\u0644") # بال كال + + __articles_2len = ("\u0627\u0644", "\u0644\u0644") # ال لل + + # Prepositions letters + __prepositions1 = ("\u0643", "\u0644") # ك، ل + __prepositions2 = ("\u0628\u0628", "\u0643\u0643") # بب، كك + + is_verb = True + is_noun = True + is_defined = False + + suffixes_verb_step1_success = False + suffix_verb_step2a_success = False + suffix_verb_step2b_success = False + suffix_noun_step2c2_success = False + suffix_noun_step1a_success = False + suffix_noun_step2a_success = False + suffix_noun_step2b_success = False + suffixe_noun_step1b_success = False + prefix_step2a_success = False + prefix_step3a_noun_success = False + prefix_step3b_noun_success = False + + def __normalize_pre(self, token): + """ + :param token: string + :return: normalized token type string + """ + # strip diacritics + token = self.__vocalization.sub("", token) + # strip kasheeda + token = self.__kasheeda.sub("", token) + # strip punctuation marks + token = self.__arabic_punctuation_marks.sub("", token) + return token + + def __normalize_post(self, token): + # normalize last hamza + for hamza in self.__last_hamzat: + if token.endswith(hamza): + token = suffix_replace(token, hamza, "\u0621") + break + # normalize other hamzat + token = self.__initial_hamzat.sub("\u0627", token) + token = self.__waw_hamza.sub("\u0648", token) + token = self.__yeh_hamza.sub("\u064a", token) + token = self.__alefat.sub("\u0627", token) + return token + + def __checks_1(self, token): + for prefix in self.__checks1: + if token.startswith(prefix): + if prefix in self.__articles_3len and len(token) > 4: + self.is_noun = True + self.is_verb = False + self.is_defined = True + break + + if prefix in self.__articles_2len and len(token) > 3: + self.is_noun = True + self.is_verb = False + self.is_defined = True + break + + def __checks_2(self, token): + for suffix in self.__checks2: + if token.endswith(suffix): + if suffix == "\u0629" and len(token) > 2: + self.is_noun = True + self.is_verb = False + break + + if suffix == "\u0627\u062a" and len(token) > 3: + self.is_noun = True + self.is_verb = False + break + + def __Suffix_Verb_Step1(self, token): + for suffix in self.__suffix_verb_step1: + if token.endswith(suffix): + if suffix in self.__conjugation_suffix_verb_1 and len(token) >= 4: + token = token[:-1] + self.suffixes_verb_step1_success = True + break + + if suffix in self.__conjugation_suffix_verb_2 and len(token) >= 5: + token = token[:-2] + self.suffixes_verb_step1_success = True + break + + if suffix in self.__conjugation_suffix_verb_3 and len(token) >= 6: + token = token[:-3] + self.suffixes_verb_step1_success = True + break + return token + + def __Suffix_Verb_Step2a(self, token): + for suffix in self.__suffix_verb_step2a: + if token.endswith(suffix) and len(token) > 3: + if suffix == "\u062a" and len(token) >= 4: + token = token[:-1] + self.suffix_verb_step2a_success = True + break + + if suffix in self.__conjugation_suffix_verb_4 and len(token) >= 4: + token = token[:-1] + self.suffix_verb_step2a_success = True + break + + if suffix in self.__conjugation_suffix_verb_past and len(token) >= 5: + token = token[:-2] # past + self.suffix_verb_step2a_success = True + break + + if suffix in self.__conjugation_suffix_verb_present and len(token) > 5: + token = token[:-2] # present + self.suffix_verb_step2a_success = True + break + + if suffix == "\u062a\u0645\u0627" and len(token) >= 6: + token = token[:-3] + self.suffix_verb_step2a_success = True + break + return token + + def __Suffix_Verb_Step2c(self, token): + for suffix in self.__suffix_verb_step2c: + if token.endswith(suffix): + if suffix == "\u062a\u0645\u0648" and len(token) >= 6: + token = token[:-3] + break + + if suffix == "\u0648" and len(token) >= 4: + token = token[:-1] + break + return token + + def __Suffix_Verb_Step2b(self, token): + for suffix in self.__suffix_verb_step2b: + if token.endswith(suffix) and len(token) >= 5: + token = token[:-2] + self.suffix_verb_step2b_success = True + break + return token + + def __Suffix_Noun_Step2c2(self, token): + for suffix in self.__suffix_noun_step2c2: + if token.endswith(suffix) and len(token) >= 3: + token = token[:-1] + self.suffix_noun_step2c2_success = True + break + return token + + def __Suffix_Noun_Step1a(self, token): + for suffix in self.__suffix_noun_step1a: + if token.endswith(suffix): + if suffix in self.__conjugation_suffix_noun_1 and len(token) >= 4: + token = token[:-1] + self.suffix_noun_step1a_success = True + break + + if suffix in self.__conjugation_suffix_noun_2 and len(token) >= 5: + token = token[:-2] + self.suffix_noun_step1a_success = True + break + + if suffix in self.__conjugation_suffix_noun_3 and len(token) >= 6: + token = token[:-3] + self.suffix_noun_step1a_success = True + break + return token + + def __Suffix_Noun_Step2a(self, token): + for suffix in self.__suffix_noun_step2a: + if token.endswith(suffix) and len(token) > 4: + token = token[:-1] + self.suffix_noun_step2a_success = True + break + return token + + def __Suffix_Noun_Step2b(self, token): + for suffix in self.__suffix_noun_step2b: + if token.endswith(suffix) and len(token) >= 5: + token = token[:-2] + self.suffix_noun_step2b_success = True + break + return token + + def __Suffix_Noun_Step2c1(self, token): + for suffix in self.__suffix_noun_step2c1: + if token.endswith(suffix) and len(token) >= 4: + token = token[:-1] + break + return token + + def __Suffix_Noun_Step1b(self, token): + for suffix in self.__suffix_noun_step1b: + if token.endswith(suffix) and len(token) > 5: + token = token[:-1] + self.suffixe_noun_step1b_success = True + break + return token + + def __Suffix_Noun_Step3(self, token): + for suffix in self.__suffix_noun_step3: + if token.endswith(suffix) and len(token) >= 3: + token = token[:-1] # ya' nisbiya + break + return token + + def __Suffix_All_alef_maqsura(self, token): + for suffix in self.__suffix_all_alef_maqsura: + if token.endswith(suffix): + token = suffix_replace(token, suffix, "\u064a") + return token + + def __Prefix_Step1(self, token): + for prefix in self.__prefix_step1: + if token.startswith(prefix) and len(token) > 3: + if prefix == "\u0623\u0623": + token = prefix_replace(token, prefix, "\u0623") + break + + elif prefix == "\u0623\u0622": + token = prefix_replace(token, prefix, "\u0622") + break + + elif prefix == "\u0623\u0624": + token = prefix_replace(token, prefix, "\u0624") + break + + elif prefix == "\u0623\u0627": + token = prefix_replace(token, prefix, "\u0627") + break + + elif prefix == "\u0623\u0625": + token = prefix_replace(token, prefix, "\u0625") + break + return token + + def __Prefix_Step2a(self, token): + for prefix in self.__prefix_step2a: + if token.startswith(prefix) and len(token) > 5: + token = token[len(prefix) :] + self.prefix_step2a_success = True + break + return token + + def __Prefix_Step2b(self, token): + for prefix in self.__prefix_step2b: + if token.startswith(prefix) and len(token) > 3: + if token[:2] not in self.__prefixes1: + token = token[len(prefix) :] + break + return token + + def __Prefix_Step3a_Noun(self, token): + for prefix in self.__prefix_step3a_noun: + if token.startswith(prefix): + if prefix in self.__articles_2len and len(token) > 4: + token = token[len(prefix) :] + self.prefix_step3a_noun_success = True + break + if prefix in self.__articles_3len and len(token) > 5: + token = token[len(prefix) :] + break + return token + + def __Prefix_Step3b_Noun(self, token): + for prefix in self.__prefix_step3b_noun: + if token.startswith(prefix): + if len(token) > 3: + if prefix == "\u0628": + token = token[len(prefix) :] + self.prefix_step3b_noun_success = True + break + + if prefix in self.__prepositions2: + token = prefix_replace(token, prefix, prefix[1]) + self.prefix_step3b_noun_success = True + break + + if prefix in self.__prepositions1 and len(token) > 4: + token = token[len(prefix) :] # BUG: cause confusion + self.prefix_step3b_noun_success = True + break + return token + + def __Prefix_Step3_Verb(self, token): + for prefix in self.__prefix_step3_verb: + if token.startswith(prefix) and len(token) > 4: + token = prefix_replace(token, prefix, prefix[1]) + break + return token + + def __Prefix_Step4_Verb(self, token): + for prefix in self.__prefix_step4_verb: + if token.startswith(prefix) and len(token) > 4: + token = prefix_replace(token, prefix, "\u0627\u0633\u062a") + self.is_verb = True + self.is_noun = False + break + return token + + def stem(self, word): + """ + Stem an Arabic word and return the stemmed form. + + :param word: string + :return: string + """ + # set initial values + self.is_verb = True + self.is_noun = True + self.is_defined = False + + self.suffix_verb_step2a_success = False + self.suffix_verb_step2b_success = False + self.suffix_noun_step2c2_success = False + self.suffix_noun_step1a_success = False + self.suffix_noun_step2a_success = False + self.suffix_noun_step2b_success = False + self.suffixe_noun_step1b_success = False + self.prefix_step2a_success = False + self.prefix_step3a_noun_success = False + self.prefix_step3b_noun_success = False + + modified_word = word + # guess type and properties + # checks1 + self.__checks_1(modified_word) + # checks2 + self.__checks_2(modified_word) + # Pre_Normalization + modified_word = self.__normalize_pre(modified_word) + # Avoid stopwords + if modified_word in self.stopwords or len(modified_word) <= 2: + return modified_word + # Start stemming + if self.is_verb: + modified_word = self.__Suffix_Verb_Step1(modified_word) + if self.suffixes_verb_step1_success: + modified_word = self.__Suffix_Verb_Step2a(modified_word) + if not self.suffix_verb_step2a_success: + modified_word = self.__Suffix_Verb_Step2c(modified_word) + # or next TODO: How to deal with or next instruction + else: + modified_word = self.__Suffix_Verb_Step2b(modified_word) + if not self.suffix_verb_step2b_success: + modified_word = self.__Suffix_Verb_Step2a(modified_word) + if self.is_noun: + modified_word = self.__Suffix_Noun_Step2c2(modified_word) + if not self.suffix_noun_step2c2_success: + if not self.is_defined: + modified_word = self.__Suffix_Noun_Step1a(modified_word) + # if self.suffix_noun_step1a_success: + modified_word = self.__Suffix_Noun_Step2a(modified_word) + if not self.suffix_noun_step2a_success: + modified_word = self.__Suffix_Noun_Step2b(modified_word) + if ( + not self.suffix_noun_step2b_success + and not self.suffix_noun_step2a_success + ): + modified_word = self.__Suffix_Noun_Step2c1(modified_word) + # or next ? todo : how to deal with or next + else: + modified_word = self.__Suffix_Noun_Step1b(modified_word) + if self.suffixe_noun_step1b_success: + modified_word = self.__Suffix_Noun_Step2a(modified_word) + if not self.suffix_noun_step2a_success: + modified_word = self.__Suffix_Noun_Step2b(modified_word) + if ( + not self.suffix_noun_step2b_success + and not self.suffix_noun_step2a_success + ): + modified_word = self.__Suffix_Noun_Step2c1(modified_word) + else: + if not self.is_defined: + modified_word = self.__Suffix_Noun_Step2a(modified_word) + modified_word = self.__Suffix_Noun_Step2b(modified_word) + modified_word = self.__Suffix_Noun_Step3(modified_word) + if not self.is_noun and self.is_verb: + modified_word = self.__Suffix_All_alef_maqsura(modified_word) + + # prefixes + modified_word = self.__Prefix_Step1(modified_word) + modified_word = self.__Prefix_Step2a(modified_word) + if not self.prefix_step2a_success: + modified_word = self.__Prefix_Step2b(modified_word) + modified_word = self.__Prefix_Step3a_Noun(modified_word) + if not self.prefix_step3a_noun_success and self.is_noun: + modified_word = self.__Prefix_Step3b_Noun(modified_word) + else: + if not self.prefix_step3b_noun_success and self.is_verb: + modified_word = self.__Prefix_Step3_Verb(modified_word) + modified_word = self.__Prefix_Step4_Verb(modified_word) + + # post normalization stemming + modified_word = self.__normalize_post(modified_word) + stemmed_word = modified_word + return stemmed_word + + +class DanishStemmer(_ScandinavianStemmer): + """ + The Danish Snowball stemmer. + + :cvar __vowels: The Danish vowels. + :type __vowels: unicode + :cvar __consonants: The Danish consonants. + :type __consonants: unicode + :cvar __double_consonants: The Danish double consonants. + :type __double_consonants: tuple + :cvar __s_ending: Letters that may directly appear before a word final 's'. + :type __s_ending: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :note: A detailed description of the Danish + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/danish/stemmer.html + + """ + + # The language's vowels and other important characters are defined. + __vowels = "aeiouy\xe6\xe5\xf8" + __consonants = "bcdfghjklmnpqrstvwxz" + __double_consonants = ( + "bb", + "cc", + "dd", + "ff", + "gg", + "hh", + "jj", + "kk", + "ll", + "mm", + "nn", + "pp", + "qq", + "rr", + "ss", + "tt", + "vv", + "ww", + "xx", + "zz", + ) + __s_ending = "abcdfghjklmnoprtvyz\xe5" + + # The different suffixes, divided into the algorithm's steps + # and organized by length, are listed in tuples. + __step1_suffixes = ( + "erendes", + "erende", + "hedens", + "ethed", + "erede", + "heden", + "heder", + "endes", + "ernes", + "erens", + "erets", + "ered", + "ende", + "erne", + "eren", + "erer", + "heds", + "enes", + "eres", + "eret", + "hed", + "ene", + "ere", + "ens", + "ers", + "ets", + "en", + "er", + "es", + "et", + "e", + "s", + ) + __step2_suffixes = ("gd", "dt", "gt", "kt") + __step3_suffixes = ("elig", "l\xf8st", "lig", "els", "ig") + + def stem(self, word): + """ + Stem a Danish word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + # Every word is put into lower case for normalization. + word = word.lower() + + if word in self.stopwords: + return word + + # After this, the required regions are generated + # by the respective helper method. + r1 = self._r1_scandinavian(word, self.__vowels) + + # Then the actual stemming process starts. + # Every new step is explicitly indicated + # according to the descriptions on the Snowball website. + + # STEP 1 + for suffix in self.__step1_suffixes: + if r1.endswith(suffix): + if suffix == "s": + if word[-2] in self.__s_ending: + word = word[:-1] + r1 = r1[:-1] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 2 + for suffix in self.__step2_suffixes: + if r1.endswith(suffix): + word = word[:-1] + r1 = r1[:-1] + break + + # STEP 3 + if r1.endswith("igst"): + word = word[:-2] + r1 = r1[:-2] + + for suffix in self.__step3_suffixes: + if r1.endswith(suffix): + if suffix == "l\xf8st": + word = word[:-1] + r1 = r1[:-1] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + + if r1.endswith(self.__step2_suffixes): + word = word[:-1] + r1 = r1[:-1] + break + + # STEP 4: Undouble + for double_cons in self.__double_consonants: + if word.endswith(double_cons) and len(word) > 3: + word = word[:-1] + break + + return word + + +class DutchStemmer(_StandardStemmer): + """ + The Dutch Snowball stemmer. + + :cvar __vowels: The Dutch vowels. + :type __vowels: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step3b_suffixes: Suffixes to be deleted in step 3b of the algorithm. + :type __step3b_suffixes: tuple + :note: A detailed description of the Dutch + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/dutch/stemmer.html + + """ + + __vowels = "aeiouy\xe8" + __step1_suffixes = ("heden", "ene", "en", "se", "s") + __step3b_suffixes = ("baar", "lijk", "bar", "end", "ing", "ig") + + def stem(self, word): + """ + Stem a Dutch word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step2_success = False + + # Vowel accents are removed. + word = ( + word.replace("\xe4", "a") + .replace("\xe1", "a") + .replace("\xeb", "e") + .replace("\xe9", "e") + .replace("\xed", "i") + .replace("\xef", "i") + .replace("\xf6", "o") + .replace("\xf3", "o") + .replace("\xfc", "u") + .replace("\xfa", "u") + ) + + # An initial 'y', a 'y' after a vowel, + # and an 'i' between self.__vowels is put into upper case. + # As from now these are treated as consonants. + if word.startswith("y"): + word = "".join(("Y", word[1:])) + + for i in range(1, len(word)): + if word[i - 1] in self.__vowels and word[i] == "y": + word = "".join((word[:i], "Y", word[i + 1 :])) + + for i in range(1, len(word) - 1): + if ( + word[i - 1] in self.__vowels + and word[i] == "i" + and word[i + 1] in self.__vowels + ): + word = "".join((word[:i], "I", word[i + 1 :])) + + r1, r2 = self._r1r2_standard(word, self.__vowels) + + # R1 is adjusted so that the region before it + # contains at least 3 letters. + for i in range(1, len(word)): + if word[i] not in self.__vowels and word[i - 1] in self.__vowels: + if 3 > len(word[: i + 1]) > 0: + r1 = word[3:] + elif len(word[: i + 1]) == 0: + return word + break + + # STEP 1 + for suffix in self.__step1_suffixes: + if r1.endswith(suffix): + if suffix == "heden": + word = suffix_replace(word, suffix, "heid") + r1 = suffix_replace(r1, suffix, "heid") + if r2.endswith("heden"): + r2 = suffix_replace(r2, suffix, "heid") + + elif ( + suffix in ("ene", "en") + and not word.endswith("heden") + and word[-len(suffix) - 1] not in self.__vowels + and word[-len(suffix) - 3 : -len(suffix)] != "gem" + ): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + if word.endswith(("kk", "dd", "tt")): + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + elif ( + suffix in ("se", "s") + and word[-len(suffix) - 1] not in self.__vowels + and word[-len(suffix) - 1] != "j" + ): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 2 + if r1.endswith("e") and word[-2] not in self.__vowels: + step2_success = True + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + if word.endswith(("kk", "dd", "tt")): + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + # STEP 3a + if r2.endswith("heid") and word[-5] != "c": + word = word[:-4] + r1 = r1[:-4] + r2 = r2[:-4] + + if ( + r1.endswith("en") + and word[-3] not in self.__vowels + and word[-5:-2] != "gem" + ): + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + if word.endswith(("kk", "dd", "tt")): + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + # STEP 3b: Derivational suffixes + for suffix in self.__step3b_suffixes: + if r2.endswith(suffix): + if suffix in ("end", "ing"): + word = word[:-3] + r2 = r2[:-3] + + if r2.endswith("ig") and word[-3] != "e": + word = word[:-2] + else: + if word.endswith(("kk", "dd", "tt")): + word = word[:-1] + + elif suffix == "ig" and word[-3] != "e": + word = word[:-2] + + elif suffix == "lijk": + word = word[:-4] + r1 = r1[:-4] + + if r1.endswith("e") and word[-2] not in self.__vowels: + word = word[:-1] + if word.endswith(("kk", "dd", "tt")): + word = word[:-1] + + elif suffix == "baar": + word = word[:-4] + + elif suffix == "bar" and step2_success: + word = word[:-3] + break + + # STEP 4: Undouble vowel + if len(word) >= 4: + if word[-1] not in self.__vowels and word[-1] != "I": + if word[-3:-1] in ("aa", "ee", "oo", "uu"): + if word[-4] not in self.__vowels: + word = "".join((word[:-3], word[-3], word[-1])) + + # All occurrences of 'I' and 'Y' are put back into lower case. + word = word.replace("I", "i").replace("Y", "y") + + return word + + +class EnglishStemmer(_StandardStemmer): + """ + The English Snowball stemmer. + + :cvar __vowels: The English vowels. + :type __vowels: unicode + :cvar __double_consonants: The English double consonants. + :type __double_consonants: tuple + :cvar __li_ending: Letters that may directly appear before a word final 'li'. + :type __li_ending: unicode + :cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. + :type __step0_suffixes: tuple + :cvar __step1a_suffixes: Suffixes to be deleted in step 1a of the algorithm. + :type __step1a_suffixes: tuple + :cvar __step1b_suffixes: Suffixes to be deleted in step 1b of the algorithm. + :type __step1b_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. + :type __step4_suffixes: tuple + :cvar __step5_suffixes: Suffixes to be deleted in step 5 of the algorithm. + :type __step5_suffixes: tuple + :cvar __special_words: A dictionary containing words + which have to be stemmed specially. + :type __special_words: dict + :note: A detailed description of the English + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/english/stemmer.html + """ + + __vowels = "aeiouy" + __double_consonants = ("bb", "dd", "ff", "gg", "mm", "nn", "pp", "rr", "tt") + __li_ending = "cdeghkmnrt" + __step0_suffixes = ("'s'", "'s", "'") + __step1a_suffixes = ("sses", "ied", "ies", "us", "ss", "s") + __step1b_suffixes = ("eedly", "ingly", "edly", "eed", "ing", "ed") + __step2_suffixes = ( + "ization", + "ational", + "fulness", + "ousness", + "iveness", + "tional", + "biliti", + "lessli", + "entli", + "ation", + "alism", + "aliti", + "ousli", + "iviti", + "fulli", + "enci", + "anci", + "abli", + "izer", + "ator", + "alli", + "bli", + "ogi", + "li", + ) + __step3_suffixes = ( + "ational", + "tional", + "alize", + "icate", + "iciti", + "ative", + "ical", + "ness", + "ful", + ) + __step4_suffixes = ( + "ement", + "ance", + "ence", + "able", + "ible", + "ment", + "ant", + "ent", + "ism", + "ate", + "iti", + "ous", + "ive", + "ize", + "ion", + "al", + "er", + "ic", + ) + __step5_suffixes = ("e", "l") + __special_words = { + "skis": "ski", + "skies": "sky", + "dying": "die", + "lying": "lie", + "tying": "tie", + "idly": "idl", + "gently": "gentl", + "ugly": "ugli", + "early": "earli", + "only": "onli", + "singly": "singl", + "sky": "sky", + "news": "news", + "howe": "howe", + "atlas": "atlas", + "cosmos": "cosmos", + "bias": "bias", + "andes": "andes", + "inning": "inning", + "innings": "inning", + "outing": "outing", + "outings": "outing", + "canning": "canning", + "cannings": "canning", + "herring": "herring", + "herrings": "herring", + "earring": "earring", + "earrings": "earring", + "proceed": "proceed", + "proceeds": "proceed", + "proceeded": "proceed", + "proceeding": "proceed", + "exceed": "exceed", + "exceeds": "exceed", + "exceeded": "exceed", + "exceeding": "exceed", + "succeed": "succeed", + "succeeds": "succeed", + "succeeded": "succeed", + "succeeding": "succeed", + } + + def stem(self, word): + """ + Stem an English word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords or len(word) <= 2: + return word + + elif word in self.__special_words: + return self.__special_words[word] + + # Map the different apostrophe characters to a single consistent one + word = ( + word.replace("\u2019", "\x27") + .replace("\u2018", "\x27") + .replace("\u201b", "\x27") + ) + + if word.startswith("\x27"): + word = word[1:] + + if word.startswith("y"): + word = "".join(("Y", word[1:])) + + for i in range(1, len(word)): + if word[i - 1] in self.__vowels and word[i] == "y": + word = "".join((word[:i], "Y", word[i + 1 :])) + + step1a_vowel_found = False + step1b_vowel_found = False + + r1 = "" + r2 = "" + + if word.startswith(("gener", "commun", "arsen")): + if word.startswith(("gener", "arsen")): + r1 = word[5:] + else: + r1 = word[6:] + + for i in range(1, len(r1)): + if r1[i] not in self.__vowels and r1[i - 1] in self.__vowels: + r2 = r1[i + 1 :] + break + else: + r1, r2 = self._r1r2_standard(word, self.__vowels) + + # STEP 0 + for suffix in self.__step0_suffixes: + if word.endswith(suffix): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 1a + for suffix in self.__step1a_suffixes: + if word.endswith(suffix): + if suffix == "sses": + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix in ("ied", "ies"): + if len(word[: -len(suffix)]) > 1: + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + else: + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + elif suffix == "s": + for letter in word[:-2]: + if letter in self.__vowels: + step1a_vowel_found = True + break + + if step1a_vowel_found: + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + break + + # STEP 1b + for suffix in self.__step1b_suffixes: + if word.endswith(suffix): + if suffix in ("eed", "eedly"): + if r1.endswith(suffix): + word = suffix_replace(word, suffix, "ee") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ee") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ee") + else: + r2 = "" + else: + for letter in word[: -len(suffix)]: + if letter in self.__vowels: + step1b_vowel_found = True + break + + if step1b_vowel_found: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + + if word.endswith(("at", "bl", "iz")): + word = "".join((word, "e")) + r1 = "".join((r1, "e")) + + if len(word) > 5 or len(r1) >= 3: + r2 = "".join((r2, "e")) + + elif word.endswith(self.__double_consonants): + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + elif ( + r1 == "" + and len(word) >= 3 + and word[-1] not in self.__vowels + and word[-1] not in "wxY" + and word[-2] in self.__vowels + and word[-3] not in self.__vowels + ) or ( + r1 == "" + and len(word) == 2 + and word[0] in self.__vowels + and word[1] not in self.__vowels + ): + word = "".join((word, "e")) + + if len(r1) > 0: + r1 = "".join((r1, "e")) + + if len(r2) > 0: + r2 = "".join((r2, "e")) + break + + # STEP 1c + if len(word) > 2 and word[-1] in "yY" and word[-2] not in self.__vowels: + word = "".join((word[:-1], "i")) + if len(r1) >= 1: + r1 = "".join((r1[:-1], "i")) + else: + r1 = "" + + if len(r2) >= 1: + r2 = "".join((r2[:-1], "i")) + else: + r2 = "" + + # STEP 2 + for suffix in self.__step2_suffixes: + if word.endswith(suffix): + if r1.endswith(suffix): + if suffix == "tional": + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix in ("enci", "anci", "abli"): + word = "".join((word[:-1], "e")) + + if len(r1) >= 1: + r1 = "".join((r1[:-1], "e")) + else: + r1 = "" + + if len(r2) >= 1: + r2 = "".join((r2[:-1], "e")) + else: + r2 = "" + + elif suffix == "entli": + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix in ("izer", "ization"): + word = suffix_replace(word, suffix, "ize") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ize") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ize") + else: + r2 = "" + + elif suffix in ("ational", "ation", "ator"): + word = suffix_replace(word, suffix, "ate") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ate") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ate") + else: + r2 = "e" + + elif suffix in ("alism", "aliti", "alli"): + word = suffix_replace(word, suffix, "al") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "al") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "al") + else: + r2 = "" + + elif suffix == "fulness": + word = word[:-4] + r1 = r1[:-4] + r2 = r2[:-4] + + elif suffix in ("ousli", "ousness"): + word = suffix_replace(word, suffix, "ous") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ous") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ous") + else: + r2 = "" + + elif suffix in ("iveness", "iviti"): + word = suffix_replace(word, suffix, "ive") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ive") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ive") + else: + r2 = "e" + + elif suffix in ("biliti", "bli"): + word = suffix_replace(word, suffix, "ble") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ble") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ble") + else: + r2 = "" + + elif suffix == "ogi" and word[-4] == "l": + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + + elif suffix in ("fulli", "lessli"): + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix == "li" and word[-3] in self.__li_ending: + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + break + + # STEP 3 + for suffix in self.__step3_suffixes: + if word.endswith(suffix): + if r1.endswith(suffix): + if suffix == "tional": + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix == "ational": + word = suffix_replace(word, suffix, "ate") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ate") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ate") + else: + r2 = "" + + elif suffix == "alize": + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + + elif suffix in ("icate", "iciti", "ical"): + word = suffix_replace(word, suffix, "ic") + + if len(r1) >= len(suffix): + r1 = suffix_replace(r1, suffix, "ic") + else: + r1 = "" + + if len(r2) >= len(suffix): + r2 = suffix_replace(r2, suffix, "ic") + else: + r2 = "" + + elif suffix in ("ful", "ness"): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + + elif suffix == "ative" and r2.endswith(suffix): + word = word[:-5] + r1 = r1[:-5] + r2 = r2[:-5] + break + + # STEP 4 + for suffix in self.__step4_suffixes: + if word.endswith(suffix): + if r2.endswith(suffix): + if suffix == "ion": + if word[-4] in "st": + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 5 + if r2.endswith("l") and word[-2] == "l": + word = word[:-1] + elif r2.endswith("e"): + word = word[:-1] + elif r1.endswith("e"): + if len(word) >= 4 and ( + word[-2] in self.__vowels + or word[-2] in "wxY" + or word[-3] not in self.__vowels + or word[-4] in self.__vowels + ): + word = word[:-1] + + word = word.replace("Y", "y") + + return word + + +class FinnishStemmer(_StandardStemmer): + """ + The Finnish Snowball stemmer. + + :cvar __vowels: The Finnish vowels. + :type __vowels: unicode + :cvar __restricted_vowels: A subset of the Finnish vowels. + :type __restricted_vowels: unicode + :cvar __long_vowels: The Finnish vowels in their long forms. + :type __long_vowels: tuple + :cvar __consonants: The Finnish consonants. + :type __consonants: unicode + :cvar __double_consonants: The Finnish double consonants. + :type __double_consonants: tuple + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. + :type __step4_suffixes: tuple + :note: A detailed description of the Finnish + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/finnish/stemmer.html + """ + + __vowels = "aeiouy\xe4\xf6" + __restricted_vowels = "aeiou\xe4\xf6" + __long_vowels = ("aa", "ee", "ii", "oo", "uu", "\xe4\xe4", "\xf6\xf6") + __consonants = "bcdfghjklmnpqrstvwxz" + __double_consonants = ( + "bb", + "cc", + "dd", + "ff", + "gg", + "hh", + "jj", + "kk", + "ll", + "mm", + "nn", + "pp", + "qq", + "rr", + "ss", + "tt", + "vv", + "ww", + "xx", + "zz", + ) + __step1_suffixes = ( + "kaan", + "k\xe4\xe4n", + "sti", + "kin", + "han", + "h\xe4n", + "ko", + "k\xf6", + "pa", + "p\xe4", + ) + __step2_suffixes = ("nsa", "ns\xe4", "mme", "nne", "si", "ni", "an", "\xe4n", "en") + __step3_suffixes = ( + "siin", + "tten", + "seen", + "han", + "hen", + "hin", + "hon", + "h\xe4n", + "h\xf6n", + "den", + "tta", + "tt\xe4", + "ssa", + "ss\xe4", + "sta", + "st\xe4", + "lla", + "ll\xe4", + "lta", + "lt\xe4", + "lle", + "ksi", + "ine", + "ta", + "t\xe4", + "na", + "n\xe4", + "a", + "\xe4", + "n", + ) + __step4_suffixes = ( + "impi", + "impa", + "imp\xe4", + "immi", + "imma", + "imm\xe4", + "mpi", + "mpa", + "mp\xe4", + "mmi", + "mma", + "mm\xe4", + "eja", + "ej\xe4", + ) + + def stem(self, word): + """ + Stem a Finnish word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step3_success = False + + r1, r2 = self._r1r2_standard(word, self.__vowels) + + # STEP 1: Particles etc. + for suffix in self.__step1_suffixes: + if r1.endswith(suffix): + if suffix == "sti": + if suffix in r2: + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + else: + if word[-len(suffix) - 1] in "ntaeiouy\xe4\xf6": + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 2: Possessives + for suffix in self.__step2_suffixes: + if r1.endswith(suffix): + if suffix == "si": + if word[-3] != "k": + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix == "ni": + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + if word.endswith("kse"): + word = suffix_replace(word, "kse", "ksi") + + if r1.endswith("kse"): + r1 = suffix_replace(r1, "kse", "ksi") + + if r2.endswith("kse"): + r2 = suffix_replace(r2, "kse", "ksi") + + elif suffix == "an": + if word[-4:-2] in ("ta", "na") or word[-5:-2] in ( + "ssa", + "sta", + "lla", + "lta", + ): + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix == "\xe4n": + if word[-4:-2] in ("t\xe4", "n\xe4") or word[-5:-2] in ( + "ss\xe4", + "st\xe4", + "ll\xe4", + "lt\xe4", + ): + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + + elif suffix == "en": + if word[-5:-2] in ("lle", "ine"): + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + else: + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + break + + # STEP 3: Cases + for suffix in self.__step3_suffixes: + if r1.endswith(suffix): + if suffix in ("han", "hen", "hin", "hon", "h\xe4n", "h\xf6n"): + if ( + (suffix == "han" and word[-4] == "a") + or (suffix == "hen" and word[-4] == "e") + or (suffix == "hin" and word[-4] == "i") + or (suffix == "hon" and word[-4] == "o") + or (suffix == "h\xe4n" and word[-4] == "\xe4") + or (suffix == "h\xf6n" and word[-4] == "\xf6") + ): + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + step3_success = True + + elif suffix in ("siin", "den", "tten"): + if ( + word[-len(suffix) - 1] == "i" + and word[-len(suffix) - 2] in self.__restricted_vowels + ): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + step3_success = True + else: + continue + + elif suffix == "seen": + if word[-6:-4] in self.__long_vowels: + word = word[:-4] + r1 = r1[:-4] + r2 = r2[:-4] + step3_success = True + else: + continue + + elif suffix in ("a", "\xe4"): + if word[-2] in self.__vowels and word[-3] in self.__consonants: + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + step3_success = True + + elif suffix in ("tta", "tt\xe4"): + if word[-4] == "e": + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + step3_success = True + + elif suffix == "n": + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + step3_success = True + + if word[-2:] == "ie" or word[-2:] in self.__long_vowels: + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + step3_success = True + break + + # STEP 4: Other endings + for suffix in self.__step4_suffixes: + if r2.endswith(suffix): + if suffix in ("mpi", "mpa", "mp\xe4", "mmi", "mma", "mm\xe4"): + if word[-5:-3] != "po": + word = word[:-3] + r1 = r1[:-3] + r2 = r2[:-3] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 5: Plurals + if step3_success and len(r1) >= 1 and r1[-1] in "ij": + word = word[:-1] + r1 = r1[:-1] + + elif ( + not step3_success + and len(r1) >= 2 + and r1[-1] == "t" + and r1[-2] in self.__vowels + ): + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + if r2.endswith("imma"): + word = word[:-4] + r1 = r1[:-4] + elif r2.endswith("mma") and r2[-5:-3] != "po": + word = word[:-3] + r1 = r1[:-3] + + # STEP 6: Tidying up + if r1[-2:] in self.__long_vowels: + word = word[:-1] + r1 = r1[:-1] + + if len(r1) >= 2 and r1[-2] in self.__consonants and r1[-1] in "a\xe4ei": + word = word[:-1] + r1 = r1[:-1] + + if r1.endswith(("oj", "uj")): + word = word[:-1] + r1 = r1[:-1] + + if r1.endswith("jo"): + word = word[:-1] + r1 = r1[:-1] + + # If the word ends with a double consonant + # followed by zero or more vowels, the last consonant is removed. + for i in range(1, len(word)): + if word[-i] in self.__vowels: + continue + else: + if i == 1: + if word[-i - 1 :] in self.__double_consonants: + word = word[:-1] + else: + if word[-i - 1 : -i + 1] in self.__double_consonants: + word = "".join((word[:-i], word[-i + 1 :])) + break + + return word + + +class FrenchStemmer(_StandardStemmer): + """ + The French Snowball stemmer. + + :cvar __vowels: The French vowels. + :type __vowels: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2a_suffixes: Suffixes to be deleted in step 2a of the algorithm. + :type __step2a_suffixes: tuple + :cvar __step2b_suffixes: Suffixes to be deleted in step 2b of the algorithm. + :type __step2b_suffixes: tuple + :cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. + :type __step4_suffixes: tuple + :note: A detailed description of the French + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/french/stemmer.html + """ + + __vowels = "aeiouy\xe2\xe0\xeb\xe9\xea\xe8\xef\xee\xf4\xfb\xf9" + __step1_suffixes = ( + "issements", + "issement", + "atrices", + "atrice", + "ateurs", + "ations", + "logies", + "usions", + "utions", + "ements", + "amment", + "emment", + "ances", + "iqUes", + "ismes", + "ables", + "istes", + "ateur", + "ation", + "logie", + "usion", + "ution", + "ences", + "ement", + "euses", + "ments", + "ance", + "iqUe", + "isme", + "able", + "iste", + "ence", + "it\xe9s", + "ives", + "eaux", + "euse", + "ment", + "eux", + "it\xe9", + "ive", + "ifs", + "aux", + "if", + ) + __step2a_suffixes = ( + "issaIent", + "issantes", + "iraIent", + "issante", + "issants", + "issions", + "irions", + "issais", + "issait", + "issant", + "issent", + "issiez", + "issons", + "irais", + "irait", + "irent", + "iriez", + "irons", + "iront", + "isses", + "issez", + "\xeemes", + "\xeetes", + "irai", + "iras", + "irez", + "isse", + "ies", + "ira", + "\xeet", + "ie", + "ir", + "is", + "it", + "i", + ) + __step2b_suffixes = ( + "eraIent", + "assions", + "erions", + "assent", + "assiez", + "\xe8rent", + "erais", + "erait", + "eriez", + "erons", + "eront", + "aIent", + "antes", + "asses", + "ions", + "erai", + "eras", + "erez", + "\xe2mes", + "\xe2tes", + "ante", + "ants", + "asse", + "\xe9es", + "era", + "iez", + "ais", + "ait", + "ant", + "\xe9e", + "\xe9s", + "er", + "ez", + "\xe2t", + "ai", + "as", + "\xe9", + "a", + ) + __step4_suffixes = ("i\xe8re", "I\xe8re", "ion", "ier", "Ier", "e", "\xeb") + + def stem(self, word): + """ + Stem a French word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step1_success = False + rv_ending_found = False + step2a_success = False + step2b_success = False + + # Every occurrence of 'u' after 'q' is put into upper case. + for i in range(1, len(word)): + if word[i - 1] == "q" and word[i] == "u": + word = "".join((word[:i], "U", word[i + 1 :])) + + # Every occurrence of 'u' and 'i' + # between vowels is put into upper case. + # Every occurrence of 'y' preceded or + # followed by a vowel is also put into upper case. + for i in range(1, len(word) - 1): + if word[i - 1] in self.__vowels and word[i + 1] in self.__vowels: + if word[i] == "u": + word = "".join((word[:i], "U", word[i + 1 :])) + + elif word[i] == "i": + word = "".join((word[:i], "I", word[i + 1 :])) + + if word[i - 1] in self.__vowels or word[i + 1] in self.__vowels: + if word[i] == "y": + word = "".join((word[:i], "Y", word[i + 1 :])) + + r1, r2 = self._r1r2_standard(word, self.__vowels) + rv = self.__rv_french(word, self.__vowels) + + # STEP 1: Standard suffix removal + for suffix in self.__step1_suffixes: + if word.endswith(suffix): + if suffix == "eaux": + word = word[:-1] + step1_success = True + + elif suffix in ("euse", "euses"): + if suffix in r2: + word = word[: -len(suffix)] + step1_success = True + + elif suffix in r1: + word = suffix_replace(word, suffix, "eux") + step1_success = True + + elif suffix in ("ement", "ements") and suffix in rv: + word = word[: -len(suffix)] + step1_success = True + + if word[-2:] == "iv" and "iv" in r2: + word = word[:-2] + + if word[-2:] == "at" and "at" in r2: + word = word[:-2] + + elif word[-3:] == "eus": + if "eus" in r2: + word = word[:-3] + elif "eus" in r1: + word = "".join((word[:-1], "x")) + + elif word[-3:] in ("abl", "iqU"): + if "abl" in r2 or "iqU" in r2: + word = word[:-3] + + elif word[-3:] in ("i\xe8r", "I\xe8r"): + if "i\xe8r" in rv or "I\xe8r" in rv: + word = "".join((word[:-3], "i")) + + elif suffix == "amment" and suffix in rv: + word = suffix_replace(word, "amment", "ant") + rv = suffix_replace(rv, "amment", "ant") + rv_ending_found = True + + elif suffix == "emment" and suffix in rv: + word = suffix_replace(word, "emment", "ent") + rv_ending_found = True + + elif ( + suffix in ("ment", "ments") + and suffix in rv + and not rv.startswith(suffix) + and rv[rv.rindex(suffix) - 1] in self.__vowels + ): + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + rv_ending_found = True + + elif suffix == "aux" and suffix in r1: + word = "".join((word[:-2], "l")) + step1_success = True + + elif ( + suffix in ("issement", "issements") + and suffix in r1 + and word[-len(suffix) - 1] not in self.__vowels + ): + word = word[: -len(suffix)] + step1_success = True + + elif ( + suffix + in ( + "ance", + "iqUe", + "isme", + "able", + "iste", + "eux", + "ances", + "iqUes", + "ismes", + "ables", + "istes", + ) + and suffix in r2 + ): + word = word[: -len(suffix)] + step1_success = True + + elif ( + suffix + in ("atrice", "ateur", "ation", "atrices", "ateurs", "ations") + and suffix in r2 + ): + word = word[: -len(suffix)] + step1_success = True + + if word[-2:] == "ic": + if "ic" in r2: + word = word[:-2] + else: + word = "".join((word[:-2], "iqU")) + + elif suffix in ("logie", "logies") and suffix in r2: + word = suffix_replace(word, suffix, "log") + step1_success = True + + elif suffix in ("usion", "ution", "usions", "utions") and suffix in r2: + word = suffix_replace(word, suffix, "u") + step1_success = True + + elif suffix in ("ence", "ences") and suffix in r2: + word = suffix_replace(word, suffix, "ent") + step1_success = True + + elif suffix in ("it\xe9", "it\xe9s") and suffix in r2: + word = word[: -len(suffix)] + step1_success = True + + if word[-4:] == "abil": + if "abil" in r2: + word = word[:-4] + else: + word = "".join((word[:-2], "l")) + + elif word[-2:] == "ic": + if "ic" in r2: + word = word[:-2] + else: + word = "".join((word[:-2], "iqU")) + + elif word[-2:] == "iv": + if "iv" in r2: + word = word[:-2] + + elif suffix in ("if", "ive", "ifs", "ives") and suffix in r2: + word = word[: -len(suffix)] + step1_success = True + + if word[-2:] == "at" and "at" in r2: + word = word[:-2] + + if word[-2:] == "ic": + if "ic" in r2: + word = word[:-2] + else: + word = "".join((word[:-2], "iqU")) + break + + # STEP 2a: Verb suffixes beginning 'i' + if not step1_success or rv_ending_found: + for suffix in self.__step2a_suffixes: + if word.endswith(suffix): + if ( + suffix in rv + and len(rv) > len(suffix) + and rv[rv.rindex(suffix) - 1] not in self.__vowels + ): + word = word[: -len(suffix)] + step2a_success = True + break + + # STEP 2b: Other verb suffixes + if not step2a_success: + for suffix in self.__step2b_suffixes: + if rv.endswith(suffix): + if suffix == "ions" and "ions" in r2: + word = word[:-4] + step2b_success = True + + elif suffix in ( + "eraIent", + "erions", + "\xe8rent", + "erais", + "erait", + "eriez", + "erons", + "eront", + "erai", + "eras", + "erez", + "\xe9es", + "era", + "iez", + "\xe9e", + "\xe9s", + "er", + "ez", + "\xe9", + ): + word = word[: -len(suffix)] + step2b_success = True + + elif suffix in ( + "assions", + "assent", + "assiez", + "aIent", + "antes", + "asses", + "\xe2mes", + "\xe2tes", + "ante", + "ants", + "asse", + "ais", + "ait", + "ant", + "\xe2t", + "ai", + "as", + "a", + ): + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + step2b_success = True + if rv.endswith("e"): + word = word[:-1] + break + + # STEP 3 + if step1_success or step2a_success or step2b_success: + if word[-1] == "Y": + word = "".join((word[:-1], "i")) + elif word[-1] == "\xe7": + word = "".join((word[:-1], "c")) + + # STEP 4: Residual suffixes + else: + if len(word) >= 2 and word[-1] == "s" and word[-2] not in "aiou\xe8s": + word = word[:-1] + + for suffix in self.__step4_suffixes: + if word.endswith(suffix): + if suffix in rv: + if suffix == "ion" and suffix in r2 and rv[-4] in "st": + word = word[:-3] + + elif suffix in ("ier", "i\xe8re", "Ier", "I\xe8re"): + word = suffix_replace(word, suffix, "i") + + elif suffix == "e": + word = word[:-1] + + elif suffix == "\xeb" and word[-3:-1] == "gu": + word = word[:-1] + break + + # STEP 5: Undouble + if word.endswith(("enn", "onn", "ett", "ell", "eill")): + word = word[:-1] + + # STEP 6: Un-accent + for i in range(1, len(word)): + if word[-i] not in self.__vowels: + i += 1 + else: + if i != 1 and word[-i] in ("\xe9", "\xe8"): + word = "".join((word[:-i], "e", word[-i + 1 :])) + break + + word = word.replace("I", "i").replace("U", "u").replace("Y", "y") + + return word + + def __rv_french(self, word, vowels): + """ + Return the region RV that is used by the French stemmer. + + If the word begins with two vowels, RV is the region after + the third letter. Otherwise, it is the region after the first + vowel not at the beginning of the word, or the end of the word + if these positions cannot be found. (Exceptionally, u'par', + u'col' or u'tap' at the beginning of a word is also taken to + define RV as the region to their right.) + + :param word: The French word whose region RV is determined. + :type word: str or unicode + :param vowels: The French vowels that are used to determine + the region RV. + :type vowels: unicode + :return: the region RV for the respective French word. + :rtype: unicode + :note: This helper method is invoked by the stem method of + the subclass FrenchStemmer. It is not to be invoked directly! + + """ + rv = "" + if len(word) >= 2: + if word.startswith(("par", "col", "tap")) or ( + word[0] in vowels and word[1] in vowels + ): + rv = word[3:] + else: + for i in range(1, len(word)): + if word[i] in vowels: + rv = word[i + 1 :] + break + + return rv + + +class GermanStemmer(_StandardStemmer): + """ + The German Snowball stemmer. + + :cvar __vowels: The German vowels. + :type __vowels: unicode + :cvar __s_ending: Letters that may directly appear before a word final 's'. + :type __s_ending: unicode + :cvar __st_ending: Letter that may directly appear before a word final 'st'. + :type __st_ending: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :note: A detailed description of the German + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/german/stemmer.html + + """ + + __vowels = "aeiouy\xe4\xf6\xfc" + __s_ending = "bdfghklmnrt" + __st_ending = "bdfghklmnt" + + __step1_suffixes = ("ern", "em", "er", "en", "es", "e", "s") + __step2_suffixes = ("est", "en", "er", "st") + __step3_suffixes = ("isch", "lich", "heit", "keit", "end", "ung", "ig", "ik") + + def stem(self, word): + """ + Stem a German word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + word = word.replace("\xdf", "ss") + + # Every occurrence of 'u' and 'y' + # between vowels is put into upper case. + for i in range(1, len(word) - 1): + if word[i - 1] in self.__vowels and word[i + 1] in self.__vowels: + if word[i] == "u": + word = "".join((word[:i], "U", word[i + 1 :])) + + elif word[i] == "y": + word = "".join((word[:i], "Y", word[i + 1 :])) + + r1, r2 = self._r1r2_standard(word, self.__vowels) + + # R1 is adjusted so that the region before it + # contains at least 3 letters. + for i in range(1, len(word)): + if word[i] not in self.__vowels and word[i - 1] in self.__vowels: + if 3 > len(word[: i + 1]) > 0: + r1 = word[3:] + elif len(word[: i + 1]) == 0: + return word + break + + # STEP 1 + for suffix in self.__step1_suffixes: + if r1.endswith(suffix): + if ( + suffix in ("en", "es", "e") + and word[-len(suffix) - 4 : -len(suffix)] == "niss" + ): + word = word[: -len(suffix) - 1] + r1 = r1[: -len(suffix) - 1] + r2 = r2[: -len(suffix) - 1] + + elif suffix == "s": + if word[-2] in self.__s_ending: + word = word[:-1] + r1 = r1[:-1] + r2 = r2[:-1] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 2 + for suffix in self.__step2_suffixes: + if r1.endswith(suffix): + if suffix == "st": + if word[-3] in self.__st_ending and len(word[:-3]) >= 3: + word = word[:-2] + r1 = r1[:-2] + r2 = r2[:-2] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + break + + # STEP 3: Derivational suffixes + for suffix in self.__step3_suffixes: + if r2.endswith(suffix): + if suffix in ("end", "ung"): + if ( + "ig" in r2[-len(suffix) - 2 : -len(suffix)] + and "e" not in r2[-len(suffix) - 3 : -len(suffix) - 2] + ): + word = word[: -len(suffix) - 2] + else: + word = word[: -len(suffix)] + + elif ( + suffix in ("ig", "ik", "isch") + and "e" not in r2[-len(suffix) - 1 : -len(suffix)] + ): + word = word[: -len(suffix)] + + elif suffix in ("lich", "heit"): + if ( + "er" in r1[-len(suffix) - 2 : -len(suffix)] + or "en" in r1[-len(suffix) - 2 : -len(suffix)] + ): + word = word[: -len(suffix) - 2] + else: + word = word[: -len(suffix)] + + elif suffix == "keit": + if "lich" in r2[-len(suffix) - 4 : -len(suffix)]: + word = word[: -len(suffix) - 4] + + elif "ig" in r2[-len(suffix) - 2 : -len(suffix)]: + word = word[: -len(suffix) - 2] + else: + word = word[: -len(suffix)] + break + + # Umlaut accents are removed and + # 'u' and 'y' are put back into lower case. + word = ( + word.replace("\xe4", "a") + .replace("\xf6", "o") + .replace("\xfc", "u") + .replace("U", "u") + .replace("Y", "y") + ) + + return word + + +class HungarianStemmer(_LanguageSpecificStemmer): + """ + The Hungarian Snowball stemmer. + + :cvar __vowels: The Hungarian vowels. + :type __vowels: unicode + :cvar __digraphs: The Hungarian digraphs. + :type __digraphs: tuple + :cvar __double_consonants: The Hungarian double consonants. + :type __double_consonants: tuple + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. + :type __step4_suffixes: tuple + :cvar __step5_suffixes: Suffixes to be deleted in step 5 of the algorithm. + :type __step5_suffixes: tuple + :cvar __step6_suffixes: Suffixes to be deleted in step 6 of the algorithm. + :type __step6_suffixes: tuple + :cvar __step7_suffixes: Suffixes to be deleted in step 7 of the algorithm. + :type __step7_suffixes: tuple + :cvar __step8_suffixes: Suffixes to be deleted in step 8 of the algorithm. + :type __step8_suffixes: tuple + :cvar __step9_suffixes: Suffixes to be deleted in step 9 of the algorithm. + :type __step9_suffixes: tuple + :note: A detailed description of the Hungarian + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/hungarian/stemmer.html + + """ + + __vowels = "aeiou\xf6\xfc\xe1\xe9\xed\xf3\xf5\xfa\xfb" + __digraphs = ("cs", "dz", "dzs", "gy", "ly", "ny", "ty", "zs") + __double_consonants = ( + "bb", + "cc", + "ccs", + "dd", + "ff", + "gg", + "ggy", + "jj", + "kk", + "ll", + "lly", + "mm", + "nn", + "nny", + "pp", + "rr", + "ss", + "ssz", + "tt", + "tty", + "vv", + "zz", + "zzs", + ) + + __step1_suffixes = ("al", "el") + __step2_suffixes = ( + "k\xe9ppen", + "onk\xe9nt", + "enk\xe9nt", + "ank\xe9nt", + "k\xe9pp", + "k\xe9nt", + "ban", + "ben", + "nak", + "nek", + "val", + "vel", + "t\xf3l", + "t\xf5l", + "r\xf3l", + "r\xf5l", + "b\xf3l", + "b\xf5l", + "hoz", + "hez", + "h\xf6z", + "n\xe1l", + "n\xe9l", + "\xe9rt", + "kor", + "ba", + "be", + "ra", + "re", + "ig", + "at", + "et", + "ot", + "\xf6t", + "ul", + "\xfcl", + "v\xe1", + "v\xe9", + "en", + "on", + "an", + "\xf6n", + "n", + "t", + ) + __step3_suffixes = ("\xe1nk\xe9nt", "\xe1n", "\xe9n") + __step4_suffixes = ( + "astul", + "est\xfcl", + "\xe1stul", + "\xe9st\xfcl", + "stul", + "st\xfcl", + ) + __step5_suffixes = ("\xe1", "\xe9") + __step6_suffixes = ( + "ok\xe9", + "\xf6k\xe9", + "ak\xe9", + "ek\xe9", + "\xe1k\xe9", + "\xe1\xe9i", + "\xe9k\xe9", + "\xe9\xe9i", + "k\xe9", + "\xe9i", + "\xe9\xe9", + "\xe9", + ) + __step7_suffixes = ( + "\xe1juk", + "\xe9j\xfck", + "\xfcnk", + "unk", + "juk", + "j\xfck", + "\xe1nk", + "\xe9nk", + "nk", + "uk", + "\xfck", + "em", + "om", + "am", + "od", + "ed", + "ad", + "\xf6d", + "ja", + "je", + "\xe1m", + "\xe1d", + "\xe9m", + "\xe9d", + "m", + "d", + "a", + "e", + "o", + "\xe1", + "\xe9", + ) + __step8_suffixes = ( + "jaitok", + "jeitek", + "jaink", + "jeink", + "aitok", + "eitek", + "\xe1itok", + "\xe9itek", + "jaim", + "jeim", + "jaid", + "jeid", + "eink", + "aink", + "itek", + "jeik", + "jaik", + "\xe1ink", + "\xe9ink", + "aim", + "eim", + "aid", + "eid", + "jai", + "jei", + "ink", + "aik", + "eik", + "\xe1im", + "\xe1id", + "\xe1ik", + "\xe9im", + "\xe9id", + "\xe9ik", + "im", + "id", + "ai", + "ei", + "ik", + "\xe1i", + "\xe9i", + "i", + ) + __step9_suffixes = ("\xe1k", "\xe9k", "\xf6k", "ok", "ek", "ak", "k") + + def stem(self, word): + """ + Stem an Hungarian word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + r1 = self.__r1_hungarian(word, self.__vowels, self.__digraphs) + + # STEP 1: Remove instrumental case + if r1.endswith(self.__step1_suffixes): + for double_cons in self.__double_consonants: + if word[-2 - len(double_cons) : -2] == double_cons: + word = "".join((word[:-4], word[-3])) + + if r1[-2 - len(double_cons) : -2] == double_cons: + r1 = "".join((r1[:-4], r1[-3])) + break + + # STEP 2: Remove frequent cases + for suffix in self.__step2_suffixes: + if word.endswith(suffix): + if r1.endswith(suffix): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + + if r1.endswith("\xe1"): + word = "".join((word[:-1], "a")) + r1 = suffix_replace(r1, "\xe1", "a") + + elif r1.endswith("\xe9"): + word = "".join((word[:-1], "e")) + r1 = suffix_replace(r1, "\xe9", "e") + break + + # STEP 3: Remove special cases + for suffix in self.__step3_suffixes: + if r1.endswith(suffix): + if suffix == "\xe9n": + word = suffix_replace(word, suffix, "e") + r1 = suffix_replace(r1, suffix, "e") + else: + word = suffix_replace(word, suffix, "a") + r1 = suffix_replace(r1, suffix, "a") + break + + # STEP 4: Remove other cases + for suffix in self.__step4_suffixes: + if r1.endswith(suffix): + if suffix == "\xe1stul": + word = suffix_replace(word, suffix, "a") + r1 = suffix_replace(r1, suffix, "a") + + elif suffix == "\xe9st\xfcl": + word = suffix_replace(word, suffix, "e") + r1 = suffix_replace(r1, suffix, "e") + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 5: Remove factive case + for suffix in self.__step5_suffixes: + if r1.endswith(suffix): + for double_cons in self.__double_consonants: + if word[-1 - len(double_cons) : -1] == double_cons: + word = "".join((word[:-3], word[-2])) + + if r1[-1 - len(double_cons) : -1] == double_cons: + r1 = "".join((r1[:-3], r1[-2])) + break + + # STEP 6: Remove owned + for suffix in self.__step6_suffixes: + if r1.endswith(suffix): + if suffix in ("\xe1k\xe9", "\xe1\xe9i"): + word = suffix_replace(word, suffix, "a") + r1 = suffix_replace(r1, suffix, "a") + + elif suffix in ("\xe9k\xe9", "\xe9\xe9i", "\xe9\xe9"): + word = suffix_replace(word, suffix, "e") + r1 = suffix_replace(r1, suffix, "e") + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 7: Remove singular owner suffixes + for suffix in self.__step7_suffixes: + if word.endswith(suffix): + if r1.endswith(suffix): + if suffix in ("\xe1nk", "\xe1juk", "\xe1m", "\xe1d", "\xe1"): + word = suffix_replace(word, suffix, "a") + r1 = suffix_replace(r1, suffix, "a") + + elif suffix in ("\xe9nk", "\xe9j\xfck", "\xe9m", "\xe9d", "\xe9"): + word = suffix_replace(word, suffix, "e") + r1 = suffix_replace(r1, suffix, "e") + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 8: Remove plural owner suffixes + for suffix in self.__step8_suffixes: + if word.endswith(suffix): + if r1.endswith(suffix): + if suffix in ( + "\xe1im", + "\xe1id", + "\xe1i", + "\xe1ink", + "\xe1itok", + "\xe1ik", + ): + word = suffix_replace(word, suffix, "a") + r1 = suffix_replace(r1, suffix, "a") + + elif suffix in ( + "\xe9im", + "\xe9id", + "\xe9i", + "\xe9ink", + "\xe9itek", + "\xe9ik", + ): + word = suffix_replace(word, suffix, "e") + r1 = suffix_replace(r1, suffix, "e") + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 9: Remove plural suffixes + for suffix in self.__step9_suffixes: + if word.endswith(suffix): + if r1.endswith(suffix): + if suffix == "\xe1k": + word = suffix_replace(word, suffix, "a") + elif suffix == "\xe9k": + word = suffix_replace(word, suffix, "e") + else: + word = word[: -len(suffix)] + break + + return word + + def __r1_hungarian(self, word, vowels, digraphs): + """ + Return the region R1 that is used by the Hungarian stemmer. + + If the word begins with a vowel, R1 is defined as the region + after the first consonant or digraph (= two letters stand for + one phoneme) in the word. If the word begins with a consonant, + it is defined as the region after the first vowel in the word. + If the word does not contain both a vowel and consonant, R1 + is the null region at the end of the word. + + :param word: The Hungarian word whose region R1 is determined. + :type word: str or unicode + :param vowels: The Hungarian vowels that are used to determine + the region R1. + :type vowels: unicode + :param digraphs: The digraphs that are used to determine the + region R1. + :type digraphs: tuple + :return: the region R1 for the respective word. + :rtype: unicode + :note: This helper method is invoked by the stem method of the subclass + HungarianStemmer. It is not to be invoked directly! + + """ + r1 = "" + if word[0] in vowels: + for digraph in digraphs: + if digraph in word[1:]: + r1 = word[word.index(digraph[-1]) + 1 :] + return r1 + + for i in range(1, len(word)): + if word[i] not in vowels: + r1 = word[i + 1 :] + break + else: + for i in range(1, len(word)): + if word[i] in vowels: + r1 = word[i + 1 :] + break + + return r1 + + +class ItalianStemmer(_StandardStemmer): + """ + The Italian Snowball stemmer. + + :cvar __vowels: The Italian vowels. + :type __vowels: unicode + :cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. + :type __step0_suffixes: tuple + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :note: A detailed description of the Italian + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/italian/stemmer.html + + """ + + __vowels = "aeiou\xe0\xe8\xec\xf2\xf9" + __step0_suffixes = ( + "gliela", + "gliele", + "glieli", + "glielo", + "gliene", + "sene", + "mela", + "mele", + "meli", + "melo", + "mene", + "tela", + "tele", + "teli", + "telo", + "tene", + "cela", + "cele", + "celi", + "celo", + "cene", + "vela", + "vele", + "veli", + "velo", + "vene", + "gli", + "ci", + "la", + "le", + "li", + "lo", + "mi", + "ne", + "si", + "ti", + "vi", + ) + __step1_suffixes = ( + "atrice", + "atrici", + "azione", + "azioni", + "uzione", + "uzioni", + "usione", + "usioni", + "amento", + "amenti", + "imento", + "imenti", + "amente", + "abile", + "abili", + "ibile", + "ibili", + "mente", + "atore", + "atori", + "logia", + "logie", + "anza", + "anze", + "iche", + "ichi", + "ismo", + "ismi", + "ista", + "iste", + "isti", + "ist\xe0", + "ist\xe8", + "ist\xec", + "ante", + "anti", + "enza", + "enze", + "ico", + "ici", + "ica", + "ice", + "oso", + "osi", + "osa", + "ose", + "it\xe0", + "ivo", + "ivi", + "iva", + "ive", + ) + __step2_suffixes = ( + "erebbero", + "irebbero", + "assero", + "assimo", + "eranno", + "erebbe", + "eremmo", + "ereste", + "eresti", + "essero", + "iranno", + "irebbe", + "iremmo", + "ireste", + "iresti", + "iscano", + "iscono", + "issero", + "arono", + "avamo", + "avano", + "avate", + "eremo", + "erete", + "erono", + "evamo", + "evano", + "evate", + "iremo", + "irete", + "irono", + "ivamo", + "ivano", + "ivate", + "ammo", + "ando", + "asse", + "assi", + "emmo", + "enda", + "ende", + "endi", + "endo", + "erai", + "erei", + "Yamo", + "iamo", + "immo", + "irai", + "irei", + "isca", + "isce", + "isci", + "isco", + "ano", + "are", + "ata", + "ate", + "ati", + "ato", + "ava", + "avi", + "avo", + "er\xe0", + "ere", + "er\xf2", + "ete", + "eva", + "evi", + "evo", + "ir\xe0", + "ire", + "ir\xf2", + "ita", + "ite", + "iti", + "ito", + "iva", + "ivi", + "ivo", + "ono", + "uta", + "ute", + "uti", + "uto", + "ar", + "ir", + ) + + def stem(self, word): + """ + Stem an Italian word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step1_success = False + + # All acute accents are replaced by grave accents. + word = ( + word.replace("\xe1", "\xe0") + .replace("\xe9", "\xe8") + .replace("\xed", "\xec") + .replace("\xf3", "\xf2") + .replace("\xfa", "\xf9") + ) + + # Every occurrence of 'u' after 'q' + # is put into upper case. + for i in range(1, len(word)): + if word[i - 1] == "q" and word[i] == "u": + word = "".join((word[:i], "U", word[i + 1 :])) + + # Every occurrence of 'u' and 'i' + # between vowels is put into upper case. + for i in range(1, len(word) - 1): + if word[i - 1] in self.__vowels and word[i + 1] in self.__vowels: + if word[i] == "u": + word = "".join((word[:i], "U", word[i + 1 :])) + + elif word[i] == "i": + word = "".join((word[:i], "I", word[i + 1 :])) + + r1, r2 = self._r1r2_standard(word, self.__vowels) + rv = self._rv_standard(word, self.__vowels) + + # STEP 0: Attached pronoun + for suffix in self.__step0_suffixes: + if rv.endswith(suffix): + if rv[-len(suffix) - 4 : -len(suffix)] in ("ando", "endo"): + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + elif rv[-len(suffix) - 2 : -len(suffix)] in ("ar", "er", "ir"): + word = suffix_replace(word, suffix, "e") + r1 = suffix_replace(r1, suffix, "e") + r2 = suffix_replace(r2, suffix, "e") + rv = suffix_replace(rv, suffix, "e") + break + + # STEP 1: Standard suffix removal + for suffix in self.__step1_suffixes: + if word.endswith(suffix): + if suffix == "amente" and r1.endswith(suffix): + step1_success = True + word = word[:-6] + r2 = r2[:-6] + rv = rv[:-6] + + if r2.endswith("iv"): + word = word[:-2] + r2 = r2[:-2] + rv = rv[:-2] + + if r2.endswith("at"): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith(("os", "ic")): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith("abil"): + word = word[:-4] + rv = rv[:-4] + + elif suffix in ("amento", "amenti", "imento", "imenti") and rv.endswith( + suffix + ): + step1_success = True + word = word[:-6] + rv = rv[:-6] + + elif r2.endswith(suffix): + step1_success = True + if suffix in ("azione", "azioni", "atore", "atori"): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + if r2.endswith("ic"): + word = word[:-2] + rv = rv[:-2] + + elif suffix in ("logia", "logie"): + word = word[:-2] + rv = word[:-2] + + elif suffix in ("uzione", "uzioni", "usione", "usioni"): + word = word[:-5] + rv = rv[:-5] + + elif suffix in ("enza", "enze"): + word = suffix_replace(word, suffix, "te") + rv = suffix_replace(rv, suffix, "te") + + elif suffix == "it\xe0": + word = word[:-3] + r2 = r2[:-3] + rv = rv[:-3] + + if r2.endswith(("ic", "iv")): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith("abil"): + word = word[:-4] + rv = rv[:-4] + + elif suffix in ("ivo", "ivi", "iva", "ive"): + word = word[:-3] + r2 = r2[:-3] + rv = rv[:-3] + + if r2.endswith("at"): + word = word[:-2] + r2 = r2[:-2] + rv = rv[:-2] + + if r2.endswith("ic"): + word = word[:-2] + rv = rv[:-2] + else: + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 2: Verb suffixes + if not step1_success: + for suffix in self.__step2_suffixes: + if rv.endswith(suffix): + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 3a + if rv.endswith(("a", "e", "i", "o", "\xe0", "\xe8", "\xec", "\xf2")): + word = word[:-1] + rv = rv[:-1] + + if rv.endswith("i"): + word = word[:-1] + rv = rv[:-1] + + # STEP 3b + if rv.endswith(("ch", "gh")): + word = word[:-1] + + word = word.replace("I", "i").replace("U", "u") + + return word + + +class NorwegianStemmer(_ScandinavianStemmer): + """ + The Norwegian Snowball stemmer. + + :cvar __vowels: The Norwegian vowels. + :type __vowels: unicode + :cvar __s_ending: Letters that may directly appear before a word final 's'. + :type __s_ending: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :note: A detailed description of the Norwegian + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/norwegian/stemmer.html + + """ + + __vowels = "aeiouy\xe6\xe5\xf8" + __s_ending = "bcdfghjlmnoprtvyz" + __step1_suffixes = ( + "hetenes", + "hetene", + "hetens", + "heter", + "heten", + "endes", + "ande", + "ende", + "edes", + "enes", + "erte", + "ede", + "ane", + "ene", + "ens", + "ers", + "ets", + "het", + "ast", + "ert", + "en", + "ar", + "er", + "as", + "es", + "et", + "a", + "e", + "s", + ) + + __step2_suffixes = ("dt", "vt") + + __step3_suffixes = ( + "hetslov", + "eleg", + "elig", + "elov", + "slov", + "leg", + "eig", + "lig", + "els", + "lov", + "ig", + ) + + def stem(self, word): + """ + Stem a Norwegian word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + r1 = self._r1_scandinavian(word, self.__vowels) + + # STEP 1 + for suffix in self.__step1_suffixes: + if r1.endswith(suffix): + if suffix in ("erte", "ert"): + word = suffix_replace(word, suffix, "er") + r1 = suffix_replace(r1, suffix, "er") + + elif suffix == "s": + if word[-2] in self.__s_ending or ( + word[-2] == "k" and word[-3] not in self.__vowels + ): + word = word[:-1] + r1 = r1[:-1] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 2 + for suffix in self.__step2_suffixes: + if r1.endswith(suffix): + word = word[:-1] + r1 = r1[:-1] + break + + # STEP 3 + for suffix in self.__step3_suffixes: + if r1.endswith(suffix): + word = word[: -len(suffix)] + break + + return word + + +class PortugueseStemmer(_StandardStemmer): + """ + The Portuguese Snowball stemmer. + + :cvar __vowels: The Portuguese vowels. + :type __vowels: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step4_suffixes: Suffixes to be deleted in step 4 of the algorithm. + :type __step4_suffixes: tuple + :note: A detailed description of the Portuguese + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/portuguese/stemmer.html + + """ + + __vowels = "aeiou\xe1\xe9\xed\xf3\xfa\xe2\xea\xf4" + __step1_suffixes = ( + "amentos", + "imentos", + "uço~es", + "amento", + "imento", + "adoras", + "adores", + "a\xe7o~es", + "logias", + "\xeancias", + "amente", + "idades", + "an\xe7as", + "ismos", + "istas", + "adora", + "a\xe7a~o", + "antes", + "\xe2ncia", + "logia", + "uça~o", + "\xeancia", + "mente", + "idade", + "an\xe7a", + "ezas", + "icos", + "icas", + "ismo", + "\xe1vel", + "\xedvel", + "ista", + "osos", + "osas", + "ador", + "ante", + "ivas", + "ivos", + "iras", + "eza", + "ico", + "ica", + "oso", + "osa", + "iva", + "ivo", + "ira", + ) + __step2_suffixes = ( + "ar\xedamos", + "er\xedamos", + "ir\xedamos", + "\xe1ssemos", + "\xeassemos", + "\xedssemos", + "ar\xedeis", + "er\xedeis", + "ir\xedeis", + "\xe1sseis", + "\xe9sseis", + "\xedsseis", + "\xe1ramos", + "\xe9ramos", + "\xedramos", + "\xe1vamos", + "aremos", + "eremos", + "iremos", + "ariam", + "eriam", + "iriam", + "assem", + "essem", + "issem", + "ara~o", + "era~o", + "ira~o", + "arias", + "erias", + "irias", + "ardes", + "erdes", + "irdes", + "asses", + "esses", + "isses", + "astes", + "estes", + "istes", + "\xe1reis", + "areis", + "\xe9reis", + "ereis", + "\xedreis", + "ireis", + "\xe1veis", + "\xedamos", + "armos", + "ermos", + "irmos", + "aria", + "eria", + "iria", + "asse", + "esse", + "isse", + "aste", + "este", + "iste", + "arei", + "erei", + "irei", + "aram", + "eram", + "iram", + "avam", + "arem", + "erem", + "irem", + "ando", + "endo", + "indo", + "adas", + "idas", + "ar\xe1s", + "aras", + "er\xe1s", + "eras", + "ir\xe1s", + "avas", + "ares", + "eres", + "ires", + "\xedeis", + "ados", + "idos", + "\xe1mos", + "amos", + "emos", + "imos", + "iras", + "ada", + "ida", + "ar\xe1", + "ara", + "er\xe1", + "era", + "ir\xe1", + "ava", + "iam", + "ado", + "ido", + "ias", + "ais", + "eis", + "ira", + "ia", + "ei", + "am", + "em", + "ar", + "er", + "ir", + "as", + "es", + "is", + "eu", + "iu", + "ou", + ) + __step4_suffixes = ("os", "a", "i", "o", "\xe1", "\xed", "\xf3") + + def stem(self, word): + """ + Stem a Portuguese word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step1_success = False + step2_success = False + + word = ( + word.replace("\xe3", "a~") + .replace("\xf5", "o~") + .replace("q\xfc", "qu") + .replace("g\xfc", "gu") + ) + + r1, r2 = self._r1r2_standard(word, self.__vowels) + rv = self._rv_standard(word, self.__vowels) + + # STEP 1: Standard suffix removal + for suffix in self.__step1_suffixes: + if word.endswith(suffix): + if suffix == "amente" and r1.endswith(suffix): + step1_success = True + + word = word[:-6] + r2 = r2[:-6] + rv = rv[:-6] + + if r2.endswith("iv"): + word = word[:-2] + r2 = r2[:-2] + rv = rv[:-2] + + if r2.endswith("at"): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith(("os", "ic", "ad")): + word = word[:-2] + rv = rv[:-2] + + elif ( + suffix in ("ira", "iras") + and rv.endswith(suffix) + and word[-len(suffix) - 1 : -len(suffix)] == "e" + ): + step1_success = True + + word = suffix_replace(word, suffix, "ir") + rv = suffix_replace(rv, suffix, "ir") + + elif r2.endswith(suffix): + step1_success = True + + if suffix in ("logia", "logias"): + word = suffix_replace(word, suffix, "log") + rv = suffix_replace(rv, suffix, "log") + + elif suffix in ("uça~o", "uço~es"): + word = suffix_replace(word, suffix, "u") + rv = suffix_replace(rv, suffix, "u") + + elif suffix in ("\xeancia", "\xeancias"): + word = suffix_replace(word, suffix, "ente") + rv = suffix_replace(rv, suffix, "ente") + + elif suffix == "mente": + word = word[:-5] + r2 = r2[:-5] + rv = rv[:-5] + + if r2.endswith(("ante", "avel", "ivel")): + word = word[:-4] + rv = rv[:-4] + + elif suffix in ("idade", "idades"): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + if r2.endswith(("ic", "iv")): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith("abil"): + word = word[:-4] + rv = rv[:-4] + + elif suffix in ("iva", "ivo", "ivas", "ivos"): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + if r2.endswith("at"): + word = word[:-2] + rv = rv[:-2] + else: + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 2: Verb suffixes + if not step1_success: + for suffix in self.__step2_suffixes: + if rv.endswith(suffix): + step2_success = True + + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 3 + if step1_success or step2_success: + if rv.endswith("i") and word[-2] == "c": + word = word[:-1] + rv = rv[:-1] + + ### STEP 4: Residual suffix + if not step1_success and not step2_success: + for suffix in self.__step4_suffixes: + if rv.endswith(suffix): + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 5 + if rv.endswith(("e", "\xe9", "\xea")): + word = word[:-1] + rv = rv[:-1] + + if (word.endswith("gu") and rv.endswith("u")) or ( + word.endswith("ci") and rv.endswith("i") + ): + word = word[:-1] + + elif word.endswith("\xe7"): + word = suffix_replace(word, "\xe7", "c") + + word = word.replace("a~", "\xe3").replace("o~", "\xf5") + + return word + + +class RomanianStemmer(_StandardStemmer): + """ + The Romanian Snowball stemmer. + + :cvar __vowels: The Romanian vowels. + :type __vowels: unicode + :cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. + :type __step0_suffixes: tuple + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :note: A detailed description of the Romanian + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/romanian/stemmer.html + + """ + + __vowels = "aeiou\u0103\xe2\xee" + __step0_suffixes = ( + "iilor", + "ului", + "elor", + "iile", + "ilor", + "atei", + "a\u0163ie", + "a\u0163ia", + "aua", + "ele", + "iua", + "iei", + "ile", + "ul", + "ea", + "ii", + ) + __step1_suffixes = ( + "abilitate", + "abilitati", + "abilit\u0103\u0163i", + "ibilitate", + "abilit\u0103i", + "ivitate", + "ivitati", + "ivit\u0103\u0163i", + "icitate", + "icitati", + "icit\u0103\u0163i", + "icatori", + "ivit\u0103i", + "icit\u0103i", + "icator", + "a\u0163iune", + "atoare", + "\u0103toare", + "i\u0163iune", + "itoare", + "iciva", + "icive", + "icivi", + "iciv\u0103", + "icala", + "icale", + "icali", + "ical\u0103", + "ativa", + "ative", + "ativi", + "ativ\u0103", + "atori", + "\u0103tori", + "itiva", + "itive", + "itivi", + "itiv\u0103", + "itori", + "iciv", + "ical", + "ativ", + "ator", + "\u0103tor", + "itiv", + "itor", + ) + __step2_suffixes = ( + "abila", + "abile", + "abili", + "abil\u0103", + "ibila", + "ibile", + "ibili", + "ibil\u0103", + "atori", + "itate", + "itati", + "it\u0103\u0163i", + "abil", + "ibil", + "oasa", + "oas\u0103", + "oase", + "anta", + "ante", + "anti", + "ant\u0103", + "ator", + "it\u0103i", + "iune", + "iuni", + "isme", + "ista", + "iste", + "isti", + "ist\u0103", + "i\u015fti", + "ata", + "at\u0103", + "ati", + "ate", + "uta", + "ut\u0103", + "uti", + "ute", + "ita", + "it\u0103", + "iti", + "ite", + "ica", + "ice", + "ici", + "ic\u0103", + "osi", + "o\u015fi", + "ant", + "iva", + "ive", + "ivi", + "iv\u0103", + "ism", + "ist", + "at", + "ut", + "it", + "ic", + "os", + "iv", + ) + __step3_suffixes = ( + "seser\u0103\u0163i", + "aser\u0103\u0163i", + "iser\u0103\u0163i", + "\xe2ser\u0103\u0163i", + "user\u0103\u0163i", + "seser\u0103m", + "aser\u0103m", + "iser\u0103m", + "\xe2ser\u0103m", + "user\u0103m", + "ser\u0103\u0163i", + "sese\u015fi", + "seser\u0103", + "easc\u0103", + "ar\u0103\u0163i", + "ur\u0103\u0163i", + "ir\u0103\u0163i", + "\xe2r\u0103\u0163i", + "ase\u015fi", + "aser\u0103", + "ise\u015fi", + "iser\u0103", + "\xe2se\u015fi", + "\xe2ser\u0103", + "use\u015fi", + "user\u0103", + "ser\u0103m", + "sesem", + "indu", + "\xe2ndu", + "eaz\u0103", + "e\u015fti", + "e\u015fte", + "\u0103\u015fti", + "\u0103\u015fte", + "ea\u0163i", + "ia\u0163i", + "ar\u0103m", + "ur\u0103m", + "ir\u0103m", + "\xe2r\u0103m", + "asem", + "isem", + "\xe2sem", + "usem", + "se\u015fi", + "ser\u0103", + "sese", + "are", + "ere", + "ire", + "\xe2re", + "ind", + "\xe2nd", + "eze", + "ezi", + "esc", + "\u0103sc", + "eam", + "eai", + "eau", + "iam", + "iai", + "iau", + "a\u015fi", + "ar\u0103", + "u\u015fi", + "ur\u0103", + "i\u015fi", + "ir\u0103", + "\xe2\u015fi", + "\xe2r\u0103", + "ase", + "ise", + "\xe2se", + "use", + "a\u0163i", + "e\u0163i", + "i\u0163i", + "\xe2\u0163i", + "sei", + "ez", + "am", + "ai", + "au", + "ea", + "ia", + "ui", + "\xe2i", + "\u0103m", + "em", + "im", + "\xe2m", + "se", + ) + + def stem(self, word): + """ + Stem a Romanian word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step1_success = False + step2_success = False + + for i in range(1, len(word) - 1): + if word[i - 1] in self.__vowels and word[i + 1] in self.__vowels: + if word[i] == "u": + word = "".join((word[:i], "U", word[i + 1 :])) + + elif word[i] == "i": + word = "".join((word[:i], "I", word[i + 1 :])) + + r1, r2 = self._r1r2_standard(word, self.__vowels) + rv = self._rv_standard(word, self.__vowels) + + # STEP 0: Removal of plurals and other simplifications + for suffix in self.__step0_suffixes: + if word.endswith(suffix): + if suffix in r1: + if suffix in ("ul", "ului"): + word = word[: -len(suffix)] + + if suffix in rv: + rv = rv[: -len(suffix)] + else: + rv = "" + + elif ( + suffix == "aua" + or suffix == "atei" + or (suffix == "ile" and word[-5:-3] != "ab") + ): + word = word[:-2] + + elif suffix in ("ea", "ele", "elor"): + word = suffix_replace(word, suffix, "e") + + if suffix in rv: + rv = suffix_replace(rv, suffix, "e") + else: + rv = "" + + elif suffix in ("ii", "iua", "iei", "iile", "iilor", "ilor"): + word = suffix_replace(word, suffix, "i") + + if suffix in rv: + rv = suffix_replace(rv, suffix, "i") + else: + rv = "" + + elif suffix in ("a\u0163ie", "a\u0163ia"): + word = word[:-1] + break + + # STEP 1: Reduction of combining suffixes + while True: + replacement_done = False + + for suffix in self.__step1_suffixes: + if word.endswith(suffix): + if suffix in r1: + step1_success = True + replacement_done = True + + if suffix in ( + "abilitate", + "abilitati", + "abilit\u0103i", + "abilit\u0103\u0163i", + ): + word = suffix_replace(word, suffix, "abil") + + elif suffix == "ibilitate": + word = word[:-5] + + elif suffix in ( + "ivitate", + "ivitati", + "ivit\u0103i", + "ivit\u0103\u0163i", + ): + word = suffix_replace(word, suffix, "iv") + + elif suffix in ( + "icitate", + "icitati", + "icit\u0103i", + "icit\u0103\u0163i", + "icator", + "icatori", + "iciv", + "iciva", + "icive", + "icivi", + "iciv\u0103", + "ical", + "icala", + "icale", + "icali", + "ical\u0103", + ): + word = suffix_replace(word, suffix, "ic") + + elif suffix in ( + "ativ", + "ativa", + "ative", + "ativi", + "ativ\u0103", + "a\u0163iune", + "atoare", + "ator", + "atori", + "\u0103toare", + "\u0103tor", + "\u0103tori", + ): + word = suffix_replace(word, suffix, "at") + + if suffix in r2: + r2 = suffix_replace(r2, suffix, "at") + + elif suffix in ( + "itiv", + "itiva", + "itive", + "itivi", + "itiv\u0103", + "i\u0163iune", + "itoare", + "itor", + "itori", + ): + word = suffix_replace(word, suffix, "it") + + if suffix in r2: + r2 = suffix_replace(r2, suffix, "it") + else: + step1_success = False + break + + if not replacement_done: + break + + # STEP 2: Removal of standard suffixes + for suffix in self.__step2_suffixes: + if word.endswith(suffix): + if suffix in r2: + step2_success = True + + if suffix in ("iune", "iuni"): + if word[-5] == "\u0163": + word = "".join((word[:-5], "t")) + + elif suffix in ( + "ism", + "isme", + "ist", + "ista", + "iste", + "isti", + "ist\u0103", + "i\u015fti", + ): + word = suffix_replace(word, suffix, "ist") + + else: + word = word[: -len(suffix)] + break + + # STEP 3: Removal of verb suffixes + if not step1_success and not step2_success: + for suffix in self.__step3_suffixes: + if word.endswith(suffix): + if suffix in rv: + if suffix in ( + "seser\u0103\u0163i", + "seser\u0103m", + "ser\u0103\u0163i", + "sese\u015fi", + "seser\u0103", + "ser\u0103m", + "sesem", + "se\u015fi", + "ser\u0103", + "sese", + "a\u0163i", + "e\u0163i", + "i\u0163i", + "\xe2\u0163i", + "sei", + "\u0103m", + "em", + "im", + "\xe2m", + "se", + ): + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + else: + if ( + not rv.startswith(suffix) + and rv[rv.index(suffix) - 1] not in "aeio\u0103\xe2\xee" + ): + word = word[: -len(suffix)] + break + + # STEP 4: Removal of final vowel + for suffix in ("ie", "a", "e", "i", "\u0103"): + if word.endswith(suffix): + if suffix in rv: + word = word[: -len(suffix)] + break + + word = word.replace("I", "i").replace("U", "u") + + return word + + +class RussianStemmer(_LanguageSpecificStemmer): + """ + The Russian Snowball stemmer. + + :cvar __perfective_gerund_suffixes: Suffixes to be deleted. + :type __perfective_gerund_suffixes: tuple + :cvar __adjectival_suffixes: Suffixes to be deleted. + :type __adjectival_suffixes: tuple + :cvar __reflexive_suffixes: Suffixes to be deleted. + :type __reflexive_suffixes: tuple + :cvar __verb_suffixes: Suffixes to be deleted. + :type __verb_suffixes: tuple + :cvar __noun_suffixes: Suffixes to be deleted. + :type __noun_suffixes: tuple + :cvar __superlative_suffixes: Suffixes to be deleted. + :type __superlative_suffixes: tuple + :cvar __derivational_suffixes: Suffixes to be deleted. + :type __derivational_suffixes: tuple + :note: A detailed description of the Russian + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/russian/stemmer.html + + """ + + __perfective_gerund_suffixes = ( + "ivshis'", + "yvshis'", + "vshis'", + "ivshi", + "yvshi", + "vshi", + "iv", + "yv", + "v", + ) + __adjectival_suffixes = ( + "ui^ushchi^ui^u", + "ui^ushchi^ai^a", + "ui^ushchimi", + "ui^ushchymi", + "ui^ushchego", + "ui^ushchogo", + "ui^ushchemu", + "ui^ushchomu", + "ui^ushchikh", + "ui^ushchykh", + "ui^ushchui^u", + "ui^ushchaia", + "ui^ushchoi^u", + "ui^ushchei^u", + "i^ushchi^ui^u", + "i^ushchi^ai^a", + "ui^ushchee", + "ui^ushchie", + "ui^ushchye", + "ui^ushchoe", + "ui^ushchei`", + "ui^ushchii`", + "ui^ushchyi`", + "ui^ushchoi`", + "ui^ushchem", + "ui^ushchim", + "ui^ushchym", + "ui^ushchom", + "i^ushchimi", + "i^ushchymi", + "i^ushchego", + "i^ushchogo", + "i^ushchemu", + "i^ushchomu", + "i^ushchikh", + "i^ushchykh", + "i^ushchui^u", + "i^ushchai^a", + "i^ushchoi^u", + "i^ushchei^u", + "i^ushchee", + "i^ushchie", + "i^ushchye", + "i^ushchoe", + "i^ushchei`", + "i^ushchii`", + "i^ushchyi`", + "i^ushchoi`", + "i^ushchem", + "i^ushchim", + "i^ushchym", + "i^ushchom", + "shchi^ui^u", + "shchi^ai^a", + "ivshi^ui^u", + "ivshi^ai^a", + "yvshi^ui^u", + "yvshi^ai^a", + "shchimi", + "shchymi", + "shchego", + "shchogo", + "shchemu", + "shchomu", + "shchikh", + "shchykh", + "shchui^u", + "shchai^a", + "shchoi^u", + "shchei^u", + "ivshimi", + "ivshymi", + "ivshego", + "ivshogo", + "ivshemu", + "ivshomu", + "ivshikh", + "ivshykh", + "ivshui^u", + "ivshai^a", + "ivshoi^u", + "ivshei^u", + "yvshimi", + "yvshymi", + "yvshego", + "yvshogo", + "yvshemu", + "yvshomu", + "yvshikh", + "yvshykh", + "yvshui^u", + "yvshai^a", + "yvshoi^u", + "yvshei^u", + "vshi^ui^u", + "vshi^ai^a", + "shchee", + "shchie", + "shchye", + "shchoe", + "shchei`", + "shchii`", + "shchyi`", + "shchoi`", + "shchem", + "shchim", + "shchym", + "shchom", + "ivshee", + "ivshie", + "ivshye", + "ivshoe", + "ivshei`", + "ivshii`", + "ivshyi`", + "ivshoi`", + "ivshem", + "ivshim", + "ivshym", + "ivshom", + "yvshee", + "yvshie", + "yvshye", + "yvshoe", + "yvshei`", + "yvshii`", + "yvshyi`", + "yvshoi`", + "yvshem", + "yvshim", + "yvshym", + "yvshom", + "vshimi", + "vshymi", + "vshego", + "vshogo", + "vshemu", + "vshomu", + "vshikh", + "vshykh", + "vshui^u", + "vshai^a", + "vshoi^u", + "vshei^u", + "emi^ui^u", + "emi^ai^a", + "nni^ui^u", + "nni^ai^a", + "vshee", + "vshie", + "vshye", + "vshoe", + "vshei`", + "vshii`", + "vshyi`", + "vshoi`", + "vshem", + "vshim", + "vshym", + "vshom", + "emimi", + "emymi", + "emego", + "emogo", + "ememu", + "emomu", + "emikh", + "emykh", + "emui^u", + "emai^a", + "emoi^u", + "emei^u", + "nnimi", + "nnymi", + "nnego", + "nnogo", + "nnemu", + "nnomu", + "nnikh", + "nnykh", + "nnui^u", + "nnai^a", + "nnoi^u", + "nnei^u", + "emee", + "emie", + "emye", + "emoe", + "emei`", + "emii`", + "emyi`", + "emoi`", + "emem", + "emim", + "emym", + "emom", + "nnee", + "nnie", + "nnye", + "nnoe", + "nnei`", + "nnii`", + "nnyi`", + "nnoi`", + "nnem", + "nnim", + "nnym", + "nnom", + "i^ui^u", + "i^ai^a", + "imi", + "ymi", + "ego", + "ogo", + "emu", + "omu", + "ikh", + "ykh", + "ui^u", + "ai^a", + "oi^u", + "ei^u", + "ee", + "ie", + "ye", + "oe", + "ei`", + "ii`", + "yi`", + "oi`", + "em", + "im", + "ym", + "om", + ) + __reflexive_suffixes = ("si^a", "s'") + __verb_suffixes = ( + "esh'", + "ei`te", + "ui`te", + "ui^ut", + "ish'", + "ete", + "i`te", + "i^ut", + "nno", + "ila", + "yla", + "ena", + "ite", + "ili", + "yli", + "ilo", + "ylo", + "eno", + "i^at", + "uet", + "eny", + "it'", + "yt'", + "ui^u", + "la", + "na", + "li", + "em", + "lo", + "no", + "et", + "ny", + "t'", + "ei`", + "ui`", + "il", + "yl", + "im", + "ym", + "en", + "it", + "yt", + "i^u", + "i`", + "l", + "n", + ) + __noun_suffixes = ( + "ii^ami", + "ii^akh", + "i^ami", + "ii^am", + "i^akh", + "ami", + "iei`", + "i^am", + "iem", + "akh", + "ii^u", + "'i^u", + "ii^a", + "'i^a", + "ev", + "ov", + "ie", + "'e", + "ei", + "ii", + "ei`", + "oi`", + "ii`", + "em", + "am", + "om", + "i^u", + "i^a", + "a", + "e", + "i", + "i`", + "o", + "u", + "y", + "'", + ) + __superlative_suffixes = ("ei`she", "ei`sh") + __derivational_suffixes = ("ost'", "ost") + + def stem(self, word): + """ + Stem a Russian word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + if word in self.stopwords: + return word + + chr_exceeded = False + for i in range(len(word)): + if ord(word[i]) > 255: + chr_exceeded = True + break + + if not chr_exceeded: + return word + + word = self.__cyrillic_to_roman(word) + + step1_success = False + adjectival_removed = False + verb_removed = False + undouble_success = False + superlative_removed = False + + rv, r2 = self.__regions_russian(word) + + # Step 1 + for suffix in self.__perfective_gerund_suffixes: + if rv.endswith(suffix): + if suffix in ("v", "vshi", "vshis'"): + if ( + rv[-len(suffix) - 3 : -len(suffix)] == "i^a" + or rv[-len(suffix) - 1 : -len(suffix)] == "a" + ): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + step1_success = True + break + else: + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + step1_success = True + break + + if not step1_success: + for suffix in self.__reflexive_suffixes: + if rv.endswith(suffix): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + for suffix in self.__adjectival_suffixes: + if rv.endswith(suffix): + if suffix in ( + "i^ushchi^ui^u", + "i^ushchi^ai^a", + "i^ushchui^u", + "i^ushchai^a", + "i^ushchoi^u", + "i^ushchei^u", + "i^ushchimi", + "i^ushchymi", + "i^ushchego", + "i^ushchogo", + "i^ushchemu", + "i^ushchomu", + "i^ushchikh", + "i^ushchykh", + "shchi^ui^u", + "shchi^ai^a", + "i^ushchee", + "i^ushchie", + "i^ushchye", + "i^ushchoe", + "i^ushchei`", + "i^ushchii`", + "i^ushchyi`", + "i^ushchoi`", + "i^ushchem", + "i^ushchim", + "i^ushchym", + "i^ushchom", + "vshi^ui^u", + "vshi^ai^a", + "shchui^u", + "shchai^a", + "shchoi^u", + "shchei^u", + "emi^ui^u", + "emi^ai^a", + "nni^ui^u", + "nni^ai^a", + "shchimi", + "shchymi", + "shchego", + "shchogo", + "shchemu", + "shchomu", + "shchikh", + "shchykh", + "vshui^u", + "vshai^a", + "vshoi^u", + "vshei^u", + "shchee", + "shchie", + "shchye", + "shchoe", + "shchei`", + "shchii`", + "shchyi`", + "shchoi`", + "shchem", + "shchim", + "shchym", + "shchom", + "vshimi", + "vshymi", + "vshego", + "vshogo", + "vshemu", + "vshomu", + "vshikh", + "vshykh", + "emui^u", + "emai^a", + "emoi^u", + "emei^u", + "nnui^u", + "nnai^a", + "nnoi^u", + "nnei^u", + "vshee", + "vshie", + "vshye", + "vshoe", + "vshei`", + "vshii`", + "vshyi`", + "vshoi`", + "vshem", + "vshim", + "vshym", + "vshom", + "emimi", + "emymi", + "emego", + "emogo", + "ememu", + "emomu", + "emikh", + "emykh", + "nnimi", + "nnymi", + "nnego", + "nnogo", + "nnemu", + "nnomu", + "nnikh", + "nnykh", + "emee", + "emie", + "emye", + "emoe", + "emei`", + "emii`", + "emyi`", + "emoi`", + "emem", + "emim", + "emym", + "emom", + "nnee", + "nnie", + "nnye", + "nnoe", + "nnei`", + "nnii`", + "nnyi`", + "nnoi`", + "nnem", + "nnim", + "nnym", + "nnom", + ): + if ( + rv[-len(suffix) - 3 : -len(suffix)] == "i^a" + or rv[-len(suffix) - 1 : -len(suffix)] == "a" + ): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + adjectival_removed = True + break + else: + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + adjectival_removed = True + break + + if not adjectival_removed: + for suffix in self.__verb_suffixes: + if rv.endswith(suffix): + if suffix in ( + "la", + "na", + "ete", + "i`te", + "li", + "i`", + "l", + "em", + "n", + "lo", + "no", + "et", + "i^ut", + "ny", + "t'", + "esh'", + "nno", + ): + if ( + rv[-len(suffix) - 3 : -len(suffix)] == "i^a" + or rv[-len(suffix) - 1 : -len(suffix)] == "a" + ): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + verb_removed = True + break + else: + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + verb_removed = True + break + + if not adjectival_removed and not verb_removed: + for suffix in self.__noun_suffixes: + if rv.endswith(suffix): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # Step 2 + if rv.endswith("i"): + word = word[:-1] + r2 = r2[:-1] + + # Step 3 + for suffix in self.__derivational_suffixes: + if r2.endswith(suffix): + word = word[: -len(suffix)] + break + + # Step 4 + if word.endswith("nn"): + word = word[:-1] + undouble_success = True + + if not undouble_success: + for suffix in self.__superlative_suffixes: + if word.endswith(suffix): + word = word[: -len(suffix)] + superlative_removed = True + break + if word.endswith("nn"): + word = word[:-1] + + if not undouble_success and not superlative_removed: + if word.endswith("'"): + word = word[:-1] + + word = self.__roman_to_cyrillic(word) + + return word + + def __regions_russian(self, word): + """ + Return the regions RV and R2 which are used by the Russian stemmer. + + In any word, RV is the region after the first vowel, + or the end of the word if it contains no vowel. + + R2 is the region after the first non-vowel following + a vowel in R1, or the end of the word if there is no such non-vowel. + + R1 is the region after the first non-vowel following a vowel, + or the end of the word if there is no such non-vowel. + + :param word: The Russian word whose regions RV and R2 are determined. + :type word: str or unicode + :return: the regions RV and R2 for the respective Russian word. + :rtype: tuple + :note: This helper method is invoked by the stem method of the subclass + RussianStemmer. It is not to be invoked directly! + + """ + r1 = "" + r2 = "" + rv = "" + + vowels = ("A", "U", "E", "a", "e", "i", "o", "u", "y") + word = word.replace("i^a", "A").replace("i^u", "U").replace("e`", "E") + + for i in range(1, len(word)): + if word[i] not in vowels and word[i - 1] in vowels: + r1 = word[i + 1 :] + break + + for i in range(1, len(r1)): + if r1[i] not in vowels and r1[i - 1] in vowels: + r2 = r1[i + 1 :] + break + + for i in range(len(word)): + if word[i] in vowels: + rv = word[i + 1 :] + break + + r2 = r2.replace("A", "i^a").replace("U", "i^u").replace("E", "e`") + rv = rv.replace("A", "i^a").replace("U", "i^u").replace("E", "e`") + + return (rv, r2) + + def __cyrillic_to_roman(self, word): + """ + Transliterate a Russian word into the Roman alphabet. + + A Russian word whose letters consist of the Cyrillic + alphabet are transliterated into the Roman alphabet + in order to ease the forthcoming stemming process. + + :param word: The word that is transliterated. + :type word: unicode + :return: the transliterated word. + :rtype: unicode + :note: This helper method is invoked by the stem method of the subclass + RussianStemmer. It is not to be invoked directly! + + """ + word = ( + word.replace("\u0410", "a") + .replace("\u0430", "a") + .replace("\u0411", "b") + .replace("\u0431", "b") + .replace("\u0412", "v") + .replace("\u0432", "v") + .replace("\u0413", "g") + .replace("\u0433", "g") + .replace("\u0414", "d") + .replace("\u0434", "d") + .replace("\u0415", "e") + .replace("\u0435", "e") + .replace("\u0401", "e") + .replace("\u0451", "e") + .replace("\u0416", "zh") + .replace("\u0436", "zh") + .replace("\u0417", "z") + .replace("\u0437", "z") + .replace("\u0418", "i") + .replace("\u0438", "i") + .replace("\u0419", "i`") + .replace("\u0439", "i`") + .replace("\u041a", "k") + .replace("\u043a", "k") + .replace("\u041b", "l") + .replace("\u043b", "l") + .replace("\u041c", "m") + .replace("\u043c", "m") + .replace("\u041d", "n") + .replace("\u043d", "n") + .replace("\u041e", "o") + .replace("\u043e", "o") + .replace("\u041f", "p") + .replace("\u043f", "p") + .replace("\u0420", "r") + .replace("\u0440", "r") + .replace("\u0421", "s") + .replace("\u0441", "s") + .replace("\u0422", "t") + .replace("\u0442", "t") + .replace("\u0423", "u") + .replace("\u0443", "u") + .replace("\u0424", "f") + .replace("\u0444", "f") + .replace("\u0425", "kh") + .replace("\u0445", "kh") + .replace("\u0426", "t^s") + .replace("\u0446", "t^s") + .replace("\u0427", "ch") + .replace("\u0447", "ch") + .replace("\u0428", "sh") + .replace("\u0448", "sh") + .replace("\u0429", "shch") + .replace("\u0449", "shch") + .replace("\u042a", "''") + .replace("\u044a", "''") + .replace("\u042b", "y") + .replace("\u044b", "y") + .replace("\u042c", "'") + .replace("\u044c", "'") + .replace("\u042d", "e`") + .replace("\u044d", "e`") + .replace("\u042e", "i^u") + .replace("\u044e", "i^u") + .replace("\u042f", "i^a") + .replace("\u044f", "i^a") + ) + + return word + + def __roman_to_cyrillic(self, word): + """ + Transliterate a Russian word back into the Cyrillic alphabet. + + A Russian word formerly transliterated into the Roman alphabet + in order to ease the stemming process, is transliterated back + into the Cyrillic alphabet, its original form. + + :param word: The word that is transliterated. + :type word: str or unicode + :return: word, the transliterated word. + :rtype: unicode + :note: This helper method is invoked by the stem method of the subclass + RussianStemmer. It is not to be invoked directly! + + """ + word = ( + word.replace("i^u", "\u044e") + .replace("i^a", "\u044f") + .replace("shch", "\u0449") + .replace("kh", "\u0445") + .replace("t^s", "\u0446") + .replace("ch", "\u0447") + .replace("e`", "\u044d") + .replace("i`", "\u0439") + .replace("sh", "\u0448") + .replace("k", "\u043a") + .replace("e", "\u0435") + .replace("zh", "\u0436") + .replace("a", "\u0430") + .replace("b", "\u0431") + .replace("v", "\u0432") + .replace("g", "\u0433") + .replace("d", "\u0434") + .replace("e", "\u0435") + .replace("z", "\u0437") + .replace("i", "\u0438") + .replace("l", "\u043b") + .replace("m", "\u043c") + .replace("n", "\u043d") + .replace("o", "\u043e") + .replace("p", "\u043f") + .replace("r", "\u0440") + .replace("s", "\u0441") + .replace("t", "\u0442") + .replace("u", "\u0443") + .replace("f", "\u0444") + .replace("''", "\u044a") + .replace("y", "\u044b") + .replace("'", "\u044c") + ) + + return word + + +class SpanishStemmer(_StandardStemmer): + """ + The Spanish Snowball stemmer. + + :cvar __vowels: The Spanish vowels. + :type __vowels: unicode + :cvar __step0_suffixes: Suffixes to be deleted in step 0 of the algorithm. + :type __step0_suffixes: tuple + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2a_suffixes: Suffixes to be deleted in step 2a of the algorithm. + :type __step2a_suffixes: tuple + :cvar __step2b_suffixes: Suffixes to be deleted in step 2b of the algorithm. + :type __step2b_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :note: A detailed description of the Spanish + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/spanish/stemmer.html + + """ + + __vowels = "aeiou\xe1\xe9\xed\xf3\xfa\xfc" + __step0_suffixes = ( + "selas", + "selos", + "sela", + "selo", + "las", + "les", + "los", + "nos", + "me", + "se", + "la", + "le", + "lo", + ) + __step1_suffixes = ( + "amientos", + "imientos", + "amiento", + "imiento", + "acion", + "aciones", + "uciones", + "adoras", + "adores", + "ancias", + "log\xedas", + "encias", + "amente", + "idades", + "anzas", + "ismos", + "ables", + "ibles", + "istas", + "adora", + "aci\xf3n", + "antes", + "ancia", + "log\xeda", + "uci\xf3n", + "encia", + "mente", + "anza", + "icos", + "icas", + "ismo", + "able", + "ible", + "ista", + "osos", + "osas", + "ador", + "ante", + "idad", + "ivas", + "ivos", + "ico", + "ica", + "oso", + "osa", + "iva", + "ivo", + ) + __step2a_suffixes = ( + "yeron", + "yendo", + "yamos", + "yais", + "yan", + "yen", + "yas", + "yes", + "ya", + "ye", + "yo", + "y\xf3", + ) + __step2b_suffixes = ( + "ar\xedamos", + "er\xedamos", + "ir\xedamos", + "i\xe9ramos", + "i\xe9semos", + "ar\xedais", + "aremos", + "er\xedais", + "eremos", + "ir\xedais", + "iremos", + "ierais", + "ieseis", + "asteis", + "isteis", + "\xe1bamos", + "\xe1ramos", + "\xe1semos", + "ar\xedan", + "ar\xedas", + "ar\xe9is", + "er\xedan", + "er\xedas", + "er\xe9is", + "ir\xedan", + "ir\xedas", + "ir\xe9is", + "ieran", + "iesen", + "ieron", + "iendo", + "ieras", + "ieses", + "abais", + "arais", + "aseis", + "\xe9amos", + "ar\xe1n", + "ar\xe1s", + "ar\xeda", + "er\xe1n", + "er\xe1s", + "er\xeda", + "ir\xe1n", + "ir\xe1s", + "ir\xeda", + "iera", + "iese", + "aste", + "iste", + "aban", + "aran", + "asen", + "aron", + "ando", + "abas", + "adas", + "idas", + "aras", + "ases", + "\xedais", + "ados", + "idos", + "amos", + "imos", + "emos", + "ar\xe1", + "ar\xe9", + "er\xe1", + "er\xe9", + "ir\xe1", + "ir\xe9", + "aba", + "ada", + "ida", + "ara", + "ase", + "\xedan", + "ado", + "ido", + "\xedas", + "\xe1is", + "\xe9is", + "\xeda", + "ad", + "ed", + "id", + "an", + "i\xf3", + "ar", + "er", + "ir", + "as", + "\xeds", + "en", + "es", + ) + __step3_suffixes = ("os", "a", "e", "o", "\xe1", "\xe9", "\xed", "\xf3") + + def stem(self, word): + """ + Stem a Spanish word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + step1_success = False + + r1, r2 = self._r1r2_standard(word, self.__vowels) + rv = self._rv_standard(word, self.__vowels) + + # STEP 0: Attached pronoun + for suffix in self.__step0_suffixes: + if not (word.endswith(suffix) and rv.endswith(suffix)): + continue + + if ( + rv[: -len(suffix)].endswith( + ( + "ando", + "\xe1ndo", + "ar", + "\xe1r", + "er", + "\xe9r", + "iendo", + "i\xe9ndo", + "ir", + "\xedr", + ) + ) + ) or ( + rv[: -len(suffix)].endswith("yendo") + and word[: -len(suffix)].endswith("uyendo") + ): + word = self.__replace_accented(word[: -len(suffix)]) + r1 = self.__replace_accented(r1[: -len(suffix)]) + r2 = self.__replace_accented(r2[: -len(suffix)]) + rv = self.__replace_accented(rv[: -len(suffix)]) + break + + # STEP 1: Standard suffix removal + for suffix in self.__step1_suffixes: + if not word.endswith(suffix): + continue + + if suffix == "amente" and r1.endswith(suffix): + step1_success = True + word = word[:-6] + r2 = r2[:-6] + rv = rv[:-6] + + if r2.endswith("iv"): + word = word[:-2] + r2 = r2[:-2] + rv = rv[:-2] + + if r2.endswith("at"): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith(("os", "ic", "ad")): + word = word[:-2] + rv = rv[:-2] + + elif r2.endswith(suffix): + step1_success = True + if suffix in ( + "adora", + "ador", + "aci\xf3n", + "adoras", + "adores", + "acion", + "aciones", + "ante", + "antes", + "ancia", + "ancias", + ): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + if r2.endswith("ic"): + word = word[:-2] + rv = rv[:-2] + + elif suffix in ("log\xeda", "log\xedas"): + word = suffix_replace(word, suffix, "log") + rv = suffix_replace(rv, suffix, "log") + + elif suffix in ("uci\xf3n", "uciones"): + word = suffix_replace(word, suffix, "u") + rv = suffix_replace(rv, suffix, "u") + + elif suffix in ("encia", "encias"): + word = suffix_replace(word, suffix, "ente") + rv = suffix_replace(rv, suffix, "ente") + + elif suffix == "mente": + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + if r2.endswith(("ante", "able", "ible")): + word = word[:-4] + rv = rv[:-4] + + elif suffix in ("idad", "idades"): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + + for pre_suff in ("abil", "ic", "iv"): + if r2.endswith(pre_suff): + word = word[: -len(pre_suff)] + rv = rv[: -len(pre_suff)] + + elif suffix in ("ivo", "iva", "ivos", "ivas"): + word = word[: -len(suffix)] + r2 = r2[: -len(suffix)] + rv = rv[: -len(suffix)] + if r2.endswith("at"): + word = word[:-2] + rv = rv[:-2] + else: + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 2a: Verb suffixes beginning 'y' + if not step1_success: + for suffix in self.__step2a_suffixes: + if rv.endswith(suffix) and word[-len(suffix) - 1 : -len(suffix)] == "u": + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + break + + # STEP 2b: Other verb suffixes + for suffix in self.__step2b_suffixes: + if rv.endswith(suffix): + word = word[: -len(suffix)] + rv = rv[: -len(suffix)] + if suffix in ("en", "es", "\xe9is", "emos"): + if word.endswith("gu"): + word = word[:-1] + + if rv.endswith("gu"): + rv = rv[:-1] + break + + # STEP 3: Residual suffix + for suffix in self.__step3_suffixes: + if rv.endswith(suffix): + word = word[: -len(suffix)] + if suffix in ("e", "\xe9"): + rv = rv[: -len(suffix)] + + if word[-2:] == "gu" and rv.endswith("u"): + word = word[:-1] + break + + word = self.__replace_accented(word) + + return word + + def __replace_accented(self, word): + """ + Replaces all accented letters on a word with their non-accented + counterparts. + + :param word: A spanish word, with or without accents + :type word: str or unicode + :return: a word with the accented letters (á, é, í, ó, ú) replaced with + their non-accented counterparts (a, e, i, o, u) + :rtype: str or unicode + """ + return ( + word.replace("\xe1", "a") + .replace("\xe9", "e") + .replace("\xed", "i") + .replace("\xf3", "o") + .replace("\xfa", "u") + ) + + +class SwedishStemmer(_ScandinavianStemmer): + """ + The Swedish Snowball stemmer. + + :cvar __vowels: The Swedish vowels. + :type __vowels: unicode + :cvar __s_ending: Letters that may directly appear before a word final 's'. + :type __s_ending: unicode + :cvar __step1_suffixes: Suffixes to be deleted in step 1 of the algorithm. + :type __step1_suffixes: tuple + :cvar __step2_suffixes: Suffixes to be deleted in step 2 of the algorithm. + :type __step2_suffixes: tuple + :cvar __step3_suffixes: Suffixes to be deleted in step 3 of the algorithm. + :type __step3_suffixes: tuple + :note: A detailed description of the Swedish + stemming algorithm can be found under + http://snowball.tartarus.org/algorithms/swedish/stemmer.html + + """ + + __vowels = "aeiouy\xe4\xe5\xf6" + __s_ending = "bcdfghjklmnoprtvy" + __step1_suffixes = ( + "heterna", + "hetens", + "heter", + "heten", + "anden", + "arnas", + "ernas", + "ornas", + "andes", + "andet", + "arens", + "arna", + "erna", + "orna", + "ande", + "arne", + "aste", + "aren", + "ades", + "erns", + "ade", + "are", + "ern", + "ens", + "het", + "ast", + "ad", + "en", + "ar", + "er", + "or", + "as", + "es", + "at", + "a", + "e", + "s", + ) + __step2_suffixes = ("dd", "gd", "nn", "dt", "gt", "kt", "tt") + __step3_suffixes = ("fullt", "l\xf6st", "els", "lig", "ig") + + def stem(self, word): + """ + Stem a Swedish word and return the stemmed form. + + :param word: The word that is stemmed. + :type word: str or unicode + :return: The stemmed form. + :rtype: unicode + + """ + word = word.lower() + + if word in self.stopwords: + return word + + r1 = self._r1_scandinavian(word, self.__vowels) + + # STEP 1 + for suffix in self.__step1_suffixes: + if r1.endswith(suffix): + if suffix == "s": + if word[-2] in self.__s_ending: + word = word[:-1] + r1 = r1[:-1] + else: + word = word[: -len(suffix)] + r1 = r1[: -len(suffix)] + break + + # STEP 2 + for suffix in self.__step2_suffixes: + if r1.endswith(suffix): + word = word[:-1] + r1 = r1[:-1] + break + + # STEP 3 + for suffix in self.__step3_suffixes: + if r1.endswith(suffix): + if suffix in ("els", "lig", "ig"): + word = word[: -len(suffix)] + elif suffix in ("fullt", "l\xf6st"): + word = word[:-1] + break + + return word + + +def demo(): + """ + This function provides a demonstration of the Snowball stemmers. + + After invoking this function and specifying a language, + it stems an excerpt of the Universal Declaration of Human Rights + (which is a part of the NLTK corpus collection) and then prints + out the original and the stemmed text. + + """ + + from nltk.corpus import udhr + + udhr_corpus = { + "arabic": "Arabic_Alarabia-Arabic", + "danish": "Danish_Dansk-Latin1", + "dutch": "Dutch_Nederlands-Latin1", + "english": "English-Latin1", + "finnish": "Finnish_Suomi-Latin1", + "french": "French_Francais-Latin1", + "german": "German_Deutsch-Latin1", + "hungarian": "Hungarian_Magyar-UTF8", + "italian": "Italian_Italiano-Latin1", + "norwegian": "Norwegian-Latin1", + "porter": "English-Latin1", + "portuguese": "Portuguese_Portugues-Latin1", + "romanian": "Romanian_Romana-Latin2", + "russian": "Russian-UTF8", + "spanish": "Spanish-Latin1", + "swedish": "Swedish_Svenska-Latin1", + } + + print("\n") + print("******************************") + print("Demo for the Snowball stemmers") + print("******************************") + + while True: + language = input( + "Please enter the name of the language " + + "to be demonstrated\n" + + "/".join(SnowballStemmer.languages) + + "\n" + + "(enter 'exit' in order to leave): " + ) + + if language == "exit": + break + + if language not in SnowballStemmer.languages: + print( + "\nOops, there is no stemmer for this language. " + + "Please try again.\n" + ) + continue + + stemmer = SnowballStemmer(language) + excerpt = udhr.words(udhr_corpus[language])[:300] + + stemmed = " ".join(stemmer.stem(word) for word in excerpt) + stemmed = re.sub(r"(.{,70})\s", r"\1\n", stemmed + " ").rstrip() + excerpt = " ".join(excerpt) + excerpt = re.sub(r"(.{,70})\s", r"\1\n", excerpt + " ").rstrip() + + print("\n") + print("-" * 70) + print("ORIGINAL".center(70)) + print(excerpt) + print("\n\n") + print("STEMMED RESULTS".center(70)) + print(stemmed) + print("-" * 70) + print("\n") diff --git a/nltk/stem/util.py b/nltk/stem/util.py new file mode 100644 index 0000000..8eefa83 --- /dev/null +++ b/nltk/stem/util.py @@ -0,0 +1,25 @@ +# Natural Language Toolkit: Stemmer Utilities +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Helder +# URL: +# For license information, see LICENSE.TXT + + +def suffix_replace(original, old, new): + """ + Replaces the old suffix of the original string by a new suffix + """ + return original[: -len(old)] + new + + +def prefix_replace(original, old, new): + """ + Replaces the old prefix of the original string by a new suffix + + :param original: string + :param old: string + :param new: string + :return: string + """ + return new + original[len(old) :] diff --git a/nltk/stem/wordnet.py b/nltk/stem/wordnet.py new file mode 100644 index 0000000..61fee4a --- /dev/null +++ b/nltk/stem/wordnet.py @@ -0,0 +1,89 @@ +# Natural Language Toolkit: WordNet stemmer interface +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Steven Bird +# Edward Loper +# Eric Kafe +# URL: +# For license information, see LICENSE.TXT + + +class WordNetLemmatizer: + """ + WordNet Lemmatizer + + Provides 3 lemmatizer modes: _morphy(), morphy() and lemmatize(). + + lemmatize() is a permissive wrapper around _morphy(). + It returns the shortest lemma found in WordNet, + or the input string unchanged if nothing is found. + + >>> from nltk.stem import WordNetLemmatizer as wnl + >>> print(wnl().lemmatize('us', 'n')) + u + + >>> print(wnl().lemmatize('Anythinggoeszxcv')) + Anythinggoeszxcv + + """ + + def _morphy(self, form, pos, check_exceptions=True): + """ + _morphy() is WordNet's _morphy lemmatizer. + It returns a list of all lemmas found in WordNet. + + >>> from nltk.stem import WordNetLemmatizer as wnl + >>> print(wnl()._morphy('us', 'n')) + ['us', 'u'] + """ + from nltk.corpus import wordnet as wn + + return wn._morphy(form, pos, check_exceptions) + + def morphy(self, form, pos=None, check_exceptions=True): + """ + morphy() is a restrictive wrapper around _morphy(). + It returns the first lemma found in WordNet, + or None if no lemma is found. + + >>> from nltk.stem import WordNetLemmatizer as wnl + >>> print(wnl().morphy('us', 'n')) + us + + >>> print(wnl().morphy('catss')) + None + """ + from nltk.corpus import wordnet as wn + + return wn.morphy(form, pos, check_exceptions) + + def lemmatize(self, word: str, pos: str = "n") -> str: + """Lemmatize `word` by picking the shortest of the possible lemmas, + using the wordnet corpus reader's built-in _morphy function. + Returns the input word unchanged if it cannot be found in WordNet. + + >>> from nltk.stem import WordNetLemmatizer as wnl + >>> print(wnl().lemmatize('dogs')) + dog + >>> print(wnl().lemmatize('churches')) + church + >>> print(wnl().lemmatize('aardwolves')) + aardwolf + >>> print(wnl().lemmatize('abaci')) + abacus + >>> print(wnl().lemmatize('hardrock')) + hardrock + + :param word: The input word to lemmatize. + :type word: str + :param pos: The Part Of Speech tag. Valid options are `"n"` for nouns, + `"v"` for verbs, `"a"` for adjectives, `"r"` for adverbs and `"s"` + for satellite adjectives. + :type pos: str + :return: The shortest lemma of `word`, for the given `pos`. + """ + lemmas = self._morphy(word, pos) + return min(lemmas, key=len) if lemmas else word + + def __repr__(self): + return "" diff --git a/nltk/tabdata.py b/nltk/tabdata.py new file mode 100644 index 0000000..8484d22 --- /dev/null +++ b/nltk/tabdata.py @@ -0,0 +1,108 @@ +# Natural Language Toolkit: Encode/Decocode Data as Tab-files +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Eric Kafe +# URL: +# For license information, see LICENSE.TXT +# + + +def rm_nl(s): + if s[-1] == "\n": + return s[:-1] + return s + + +class TabEncoder: + + def list2txt(self, s): + return "\n".join(s) + + def set2txt(self, s): + return self.list2txt(list(s)) + + def tup2tab(self, tup): + return "\t".join(tup) + + def tups2tab(self, x): + return "\n".join([self.tup2tab(tup) for tup in x]) + + def dict2tab(self, d): + return self.tups2tab(d.items()) + + def ivdict2tab(self, d): + # From integer-value dictionary + return self.tups2tab([(a, str(b)) for a, b in d.items()]) + + +class TabDecoder: + + def txt2list(self, f): + return [rm_nl(x) for x in f] + + def txt2set(self, f): + return {rm_nl(x) for x in f} + + def tab2tup(self, s): + return tuple(s.split("\t")) + + def tab2tups(self, f): + return [self.tab2tup(rm_nl(x)) for x in f] + + def tab2dict(self, f): + return {a: b for a, b in self.tab2tups(f)} + + def tab2ivdict(self, f): + # To integer-value dictionary + return {a: int(b) for a, b in self.tab2tups(f)} + + +# --------------------------------------------------------------------------- +# Maxent data +# --------------------------------------------------------------------------- + + +class MaxentEncoder(TabEncoder): + + def tupdict2tab(self, d): + def rep(a, b): + if a == "wordlen": + return repr(b) + if b in [True, False, None]: + return f"repr-{b}" + return b + + return self.tups2tab( + [(a, rep(a, b), c, repr(d)) for ((a, b, c), d) in d.items()] + ) + + +class MaxentDecoder(TabDecoder): + + def tupkey2dict(self, f): + + def rep(a, b): + if a == "wordlen": + return int(b) + if b == "repr-None": + return None + if b == "repr-True": + return True + if b == "repr-False": + return False + return b + + return {(a, rep(a, b), c): int(d) for (a, b, c, d) in self.tab2tups(f)} + + +# --------------------------------------------------------------------------- +# Punkt data +# --------------------------------------------------------------------------- + + +class PunktDecoder(TabDecoder): + + def tab2intdict(self, f): + from collections import defaultdict + + return defaultdict(int, {a: int(b) for a, b in self.tab2tups(f)}) diff --git a/nltk/tag/__init__.py b/nltk/tag/__init__.py new file mode 100644 index 0000000..04bde72 --- /dev/null +++ b/nltk/tag/__init__.py @@ -0,0 +1,187 @@ +# Natural Language Toolkit: Taggers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# URL: +# For license information, see LICENSE.TXT +""" +NLTK Taggers + +This package contains classes and interfaces for part-of-speech +tagging, or simply "tagging". + +A "tag" is a case-sensitive string that specifies some property of a token, +such as its part of speech. Tagged tokens are encoded as tuples +``(tag, token)``. For example, the following tagged token combines +the word ``'fly'`` with a noun part of speech tag (``'NN'``): + + >>> tagged_tok = ('fly', 'NN') + +An off-the-shelf tagger is available for English. It uses the Penn Treebank tagset: + + >>> from nltk import pos_tag, word_tokenize + >>> pos_tag(word_tokenize("John's big idea isn't all that bad.")) # doctest: +NORMALIZE_WHITESPACE + [('John', 'NNP'), ("'s", 'POS'), ('big', 'JJ'), ('idea', 'NN'), ('is', 'VBZ'), + ("n't", 'RB'), ('all', 'PDT'), ('that', 'DT'), ('bad', 'JJ'), ('.', '.')] + +A Russian tagger is also available if you specify lang="rus". It uses +the Russian National Corpus tagset: + + >>> pos_tag(word_tokenize("Илья оторопел и дважды перечитал бумажку."), lang='rus') # doctest: +SKIP + [('Илья', 'S'), ('оторопел', 'V'), ('и', 'CONJ'), ('дважды', 'ADV'), ('перечитал', 'V'), + ('бумажку', 'S'), ('.', 'NONLEX')] + +This package defines several taggers, which take a list of tokens, +assign a tag to each one, and return the resulting list of tagged tokens. +Most of the taggers are built automatically based on a training corpus. +For example, the unigram tagger tags each word *w* by checking what +the most frequent tag for *w* was in a training corpus: + + >>> from nltk.corpus import brown + >>> from nltk.tag import UnigramTagger + >>> tagger = UnigramTagger(brown.tagged_sents(categories='news')[:500]) + >>> sent = ['Mitchell', 'decried', 'the', 'high', 'rate', 'of', 'unemployment'] + >>> for word, tag in tagger.tag(sent): + ... print(word, '->', tag) + Mitchell -> NP + decried -> None + the -> AT + high -> JJ + rate -> NN + of -> IN + unemployment -> None + +Note that words that the tagger has not seen during training receive a tag +of ``None``. + +We evaluate a tagger on data that was not seen during training: + + >>> round(tagger.accuracy(brown.tagged_sents(categories='news')[500:600]), 3) + 0.735 + +For more information, please consult chapter 5 of the NLTK Book. + +isort:skip_file +""" + +import functools + +from nltk.tag.api import TaggerI +from nltk.tag.util import str2tuple, tuple2str, untag +from nltk.tag.sequential import ( + SequentialBackoffTagger, + ContextTagger, + DefaultTagger, + NgramTagger, + UnigramTagger, + BigramTagger, + TrigramTagger, + AffixTagger, + RegexpTagger, + ClassifierBasedTagger, + ClassifierBasedPOSTagger, +) +from nltk.tag.brill import BrillTagger +from nltk.tag.brill_trainer import BrillTaggerTrainer +from nltk.tag.tnt import TnT +from nltk.tag.hunpos import HunposTagger +from nltk.tag.stanford import StanfordTagger, StanfordPOSTagger, StanfordNERTagger +from nltk.tag.hmm import HiddenMarkovModelTagger, HiddenMarkovModelTrainer +from nltk.tag.senna import SennaTagger, SennaChunkTagger, SennaNERTagger +from nltk.tag.mapping import tagset_mapping, map_tag +from nltk.tag.crf import CRFTagger +from nltk.tag.perceptron import PerceptronTagger + +from nltk.data import load, find + + +PRETRAINED_TAGGERS = { + "rus": "taggers/averaged_perceptron_tagger_rus/", + "eng": "taggers/averaged_perceptron_tagger_eng/", +} + + +@functools.lru_cache +def _get_tagger(lang=None): + if lang == "rus": + tagger = PerceptronTagger(lang=lang) + else: + tagger = PerceptronTagger() + return tagger + + +def _pos_tag(tokens, tagset=None, tagger=None, lang=None): + # Currently only supports English and Russian. + if lang not in ["eng", "rus"]: + raise NotImplementedError( + "Currently, NLTK pos_tag only supports English and Russian " + "(i.e. lang='eng' or lang='rus')" + ) + # Throws Error if tokens is of string type + elif isinstance(tokens, str): + raise TypeError("tokens: expected a list of strings, got a string") + + else: + tagged_tokens = tagger.tag(tokens) + if tagset: # Maps to the specified tagset. + if lang == "eng": + tagged_tokens = [ + (token, map_tag("en-ptb", tagset, tag)) + for (token, tag) in tagged_tokens + ] + elif lang == "rus": + # Note that the new Russian pos tags from the model contains suffixes, + # see https://github.com/nltk/nltk/issues/2151#issuecomment-430709018 + tagged_tokens = [ + (token, map_tag("ru-rnc-new", tagset, tag.partition("=")[0])) + for (token, tag) in tagged_tokens + ] + return tagged_tokens + + +def pos_tag(tokens, tagset=None, lang="eng"): + """ + Use NLTK's currently recommended part of speech tagger to + tag the given list of tokens. + + >>> from nltk.tag import pos_tag + >>> from nltk.tokenize import word_tokenize + >>> pos_tag(word_tokenize("John's big idea isn't all that bad.")) # doctest: +NORMALIZE_WHITESPACE + [('John', 'NNP'), ("'s", 'POS'), ('big', 'JJ'), ('idea', 'NN'), ('is', 'VBZ'), + ("n't", 'RB'), ('all', 'PDT'), ('that', 'DT'), ('bad', 'JJ'), ('.', '.')] + >>> pos_tag(word_tokenize("John's big idea isn't all that bad."), tagset='universal') # doctest: +NORMALIZE_WHITESPACE + [('John', 'NOUN'), ("'s", 'PRT'), ('big', 'ADJ'), ('idea', 'NOUN'), ('is', 'VERB'), + ("n't", 'ADV'), ('all', 'DET'), ('that', 'DET'), ('bad', 'ADJ'), ('.', '.')] + + NB. Use `pos_tag_sents()` for efficient tagging of more than one sentence. + + :param tokens: Sequence of tokens to be tagged + :type tokens: list(str) + :param tagset: the tagset to be used, e.g. universal, wsj, brown + :type tagset: str + :param lang: the ISO 639 code of the language, e.g. 'eng' for English, 'rus' for Russian + :type lang: str + :return: The tagged tokens + :rtype: list(tuple(str, str)) + """ + tagger = _get_tagger(lang) + return _pos_tag(tokens, tagset, tagger, lang) + + +def pos_tag_sents(sentences, tagset=None, lang="eng"): + """ + Use NLTK's currently recommended part of speech tagger to tag the + given list of sentences, each consisting of a list of tokens. + + :param sentences: List of sentences to be tagged + :type sentences: list(list(str)) + :param tagset: the tagset to be used, e.g. universal, wsj, brown + :type tagset: str + :param lang: the ISO 639 code of the language, e.g. 'eng' for English, 'rus' for Russian + :type lang: str + :return: The list of tagged sentences + :rtype: list(list(tuple(str, str))) + """ + tagger = _get_tagger(lang) + return [_pos_tag(sent, tagset, tagger, lang) for sent in sentences] diff --git a/nltk/tag/api.py b/nltk/tag/api.py new file mode 100644 index 0000000..7a5abd4 --- /dev/null +++ b/nltk/tag/api.py @@ -0,0 +1,296 @@ +# Natural Language Toolkit: Tagger Interface +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# Tom Aarsen <> +# URL: +# For license information, see LICENSE.TXT + +""" +Interface for tagging each token in a sentence with supplementary +information, such as its part of speech. +""" +from abc import ABCMeta, abstractmethod +from functools import lru_cache +from itertools import chain +from typing import Dict + +from nltk.internals import deprecated, overridden +from nltk.metrics import ConfusionMatrix, accuracy +from nltk.tag.util import untag + + +class TaggerI(metaclass=ABCMeta): + """ + A processing interface for assigning a tag to each token in a list. + Tags are case sensitive strings that identify some property of each + token, such as its part of speech or its sense. + + Some taggers require specific types for their tokens. This is + generally indicated by the use of a sub-interface to ``TaggerI``. + For example, featureset taggers, which are subclassed from + ``FeaturesetTagger``, require that each token be a ``featureset``. + + Subclasses must define: + - either ``tag()`` or ``tag_sents()`` (or both) + """ + + @abstractmethod + def tag(self, tokens): + """ + Determine the most appropriate tag sequence for the given + token sequence, and return a corresponding list of tagged + tokens. A tagged token is encoded as a tuple ``(token, tag)``. + + :rtype: list(tuple(str, str)) + """ + if overridden(self.tag_sents): + return self.tag_sents([tokens])[0] + + def tag_sents(self, sentences): + """ + Apply ``self.tag()`` to each element of *sentences*. I.e.:: + + return [self.tag(sent) for sent in sentences] + """ + return [self.tag(sent) for sent in sentences] + + @deprecated("Use accuracy(gold) instead.") + def evaluate(self, gold): + return self.accuracy(gold) + + def accuracy(self, gold): + """ + Score the accuracy of the tagger against the gold standard. + Strip the tags from the gold standard text, retag it using + the tagger, then compute the accuracy score. + + :param gold: The list of tagged sentences to score the tagger on. + :type gold: list(list(tuple(str, str))) + :rtype: float + """ + + tagged_sents = self.tag_sents(untag(sent) for sent in gold) + gold_tokens = list(chain.from_iterable(gold)) + test_tokens = list(chain.from_iterable(tagged_sents)) + return accuracy(gold_tokens, test_tokens) + + @lru_cache(maxsize=1) + def _confusion_cached(self, gold): + """ + Inner function used after ``gold`` is converted to a + ``tuple(tuple(tuple(str, str)))``. That way, we can use caching on + creating a ConfusionMatrix. + + :param gold: The list of tagged sentences to run the tagger with, + also used as the reference values in the generated confusion matrix. + :type gold: tuple(tuple(tuple(str, str))) + :rtype: ConfusionMatrix + """ + + tagged_sents = self.tag_sents(untag(sent) for sent in gold) + gold_tokens = [token for _word, token in chain.from_iterable(gold)] + test_tokens = [token for _word, token in chain.from_iterable(tagged_sents)] + return ConfusionMatrix(gold_tokens, test_tokens) + + def confusion(self, gold): + """ + Return a ConfusionMatrix with the tags from ``gold`` as the reference + values, with the predictions from ``tag_sents`` as the predicted values. + + >>> from nltk.tag import PerceptronTagger + >>> from nltk.corpus import treebank + >>> tagger = PerceptronTagger() + >>> gold_data = treebank.tagged_sents()[:10] + >>> print(tagger.confusion(gold_data)) + | - | + | N | + | O P | + | N J J N N P P R R V V V V V W | + | ' E C C D E I J J J M N N N O R P R B R T V B B B B B D ` | + | ' , - . C D T X N J R S D N P S S P $ B R P O B D G N P Z T ` | + -------+----------------------------------------------------------------------------------------------+ + '' | <1> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . | + , | .<15> . . . . . . . . . . . . . . . . . . . . . . . . . . . . . | + -NONE- | . . <.> . . 2 . . . 2 . . . 5 1 . . . . 2 . . . . . . . . . . . | + . | . . .<10> . . . . . . . . . . . . . . . . . . . . . . . . . . . | + CC | . . . . <1> . . . . . . . . . . . . . . . . . . . . . . . . . . | + CD | . . . . . <5> . . . . . . . . . . . . . . . . . . . . . . . . . | + DT | . . . . . .<20> . . . . . . . . . . . . . . . . . . . . . . . . | + EX | . . . . . . . <1> . . . . . . . . . . . . . . . . . . . . . . . | + IN | . . . . . . . .<22> . . . . . . . . . . 3 . . . . . . . . . . . | + JJ | . . . . . . . . .<16> . . . . 1 . . . . 1 . . . . . . . . . . . | + JJR | . . . . . . . . . . <.> . . . . . . . . . . . . . . . . . . . . | + JJS | . . . . . . . . . . . <1> . . . . . . . . . . . . . . . . . . . | + MD | . . . . . . . . . . . . <1> . . . . . . . . . . . . . . . . . . | + NN | . . . . . . . . . . . . .<28> 1 1 . . . . . . . . . . . . . . . | + NNP | . . . . . . . . . . . . . .<25> . . . . . . . . . . . . . . . . | + NNS | . . . . . . . . . . . . . . .<19> . . . . . . . . . . . . . . . | + POS | . . . . . . . . . . . . . . . . <1> . . . . . . . . . . . . . . | + PRP | . . . . . . . . . . . . . . . . . <4> . . . . . . . . . . . . . | + PRP$ | . . . . . . . . . . . . . . . . . . <2> . . . . . . . . . . . . | + RB | . . . . . . . . . . . . . . . . . . . <4> . . . . . . . . . . . | + RBR | . . . . . . . . . . 1 . . . . . . . . . <1> . . . . . . . . . . | + RP | . . . . . . . . . . . . . . . . . . . . . <1> . . . . . . . . . | + TO | . . . . . . . . . . . . . . . . . . . . . . <5> . . . . . . . . | + VB | . . . . . . . . . . . . . . . . . . . . . . . <3> . . . . . . . | + VBD | . . . . . . . . . . . . . 1 . . . . . . . . . . <6> . . . . . . | + VBG | . . . . . . . . . . . . . 1 . . . . . . . . . . . <4> . . . . . | + VBN | . . . . . . . . . . . . . . . . . . . . . . . . 1 . <4> . . . . | + VBP | . . . . . . . . . . . . . . . . . . . . . . . . . . . <3> . . . | + VBZ | . . . . . . . . . . . . . . . . . . . . . . . . . . . . <7> . . | + WDT | . . . . . . . . 2 . . . . . . . . . . . . . . . . . . . . <.> . | + `` | . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . <1>| + -------+----------------------------------------------------------------------------------------------+ + (row = reference; col = test) + + + :param gold: The list of tagged sentences to run the tagger with, + also used as the reference values in the generated confusion matrix. + :type gold: list(list(tuple(str, str))) + :rtype: ConfusionMatrix + """ + + return self._confusion_cached(tuple(tuple(sent) for sent in gold)) + + def recall(self, gold) -> dict[str, float]: + """ + Compute the recall for each tag from ``gold`` or from running ``tag`` + on the tokenized sentences from ``gold``. Then, return the dictionary + with mappings from tag to recall. The recall is defined as: + + - *r* = true positive / (true positive + false positive) + + :param gold: The list of tagged sentences to score the tagger on. + :type gold: list(list(tuple(str, str))) + :return: A mapping from tags to recall + :rtype: Dict[str, float] + """ + + cm = self.confusion(gold) + return {tag: cm.recall(tag) for tag in cm._values} + + def precision(self, gold): + """ + Compute the precision for each tag from ``gold`` or from running ``tag`` + on the tokenized sentences from ``gold``. Then, return the dictionary + with mappings from tag to precision. The precision is defined as: + + - *p* = true positive / (true positive + false negative) + + :param gold: The list of tagged sentences to score the tagger on. + :type gold: list(list(tuple(str, str))) + :return: A mapping from tags to precision + :rtype: Dict[str, float] + """ + + cm = self.confusion(gold) + return {tag: cm.precision(tag) for tag in cm._values} + + def f_measure(self, gold, alpha=0.5): + """ + Compute the f-measure for each tag from ``gold`` or from running ``tag`` + on the tokenized sentences from ``gold``. Then, return the dictionary + with mappings from tag to f-measure. The f-measure is the harmonic mean + of the ``precision`` and ``recall``, weighted by ``alpha``. + In particular, given the precision *p* and recall *r* defined by: + + - *p* = true positive / (true positive + false negative) + - *r* = true positive / (true positive + false positive) + + The f-measure is: + + - *1/(alpha/p + (1-alpha)/r)* + + With ``alpha = 0.5``, this reduces to: + + - *2pr / (p + r)* + + :param gold: The list of tagged sentences to score the tagger on. + :type gold: list(list(tuple(str, str))) + :param alpha: Ratio of the cost of false negative compared to false + positives. Defaults to 0.5, where the costs are equal. + :type alpha: float + :return: A mapping from tags to precision + :rtype: Dict[str, float] + """ + cm = self.confusion(gold) + return {tag: cm.f_measure(tag, alpha) for tag in cm._values} + + def evaluate_per_tag(self, gold, alpha=0.5, truncate=None, sort_by_count=False): + """Tabulate the **recall**, **precision** and **f-measure** + for each tag from ``gold`` or from running ``tag`` on the tokenized + sentences from ``gold``. + + >>> from nltk.tag import PerceptronTagger + >>> from nltk.corpus import treebank + >>> tagger = PerceptronTagger() + >>> gold_data = treebank.tagged_sents()[:10] + >>> print(tagger.evaluate_per_tag(gold_data)) + Tag | Prec. | Recall | F-measure + -------+--------+--------+----------- + '' | 1.0000 | 1.0000 | 1.0000 + , | 1.0000 | 1.0000 | 1.0000 + -NONE- | 0.0000 | 0.0000 | 0.0000 + . | 1.0000 | 1.0000 | 1.0000 + CC | 1.0000 | 1.0000 | 1.0000 + CD | 0.7143 | 1.0000 | 0.8333 + DT | 1.0000 | 1.0000 | 1.0000 + EX | 1.0000 | 1.0000 | 1.0000 + IN | 0.9167 | 0.8800 | 0.8980 + JJ | 0.8889 | 0.8889 | 0.8889 + JJR | 0.0000 | 0.0000 | 0.0000 + JJS | 1.0000 | 1.0000 | 1.0000 + MD | 1.0000 | 1.0000 | 1.0000 + NN | 0.8000 | 0.9333 | 0.8615 + NNP | 0.8929 | 1.0000 | 0.9434 + NNS | 0.9500 | 1.0000 | 0.9744 + POS | 1.0000 | 1.0000 | 1.0000 + PRP | 1.0000 | 1.0000 | 1.0000 + PRP$ | 1.0000 | 1.0000 | 1.0000 + RB | 0.4000 | 1.0000 | 0.5714 + RBR | 1.0000 | 0.5000 | 0.6667 + RP | 1.0000 | 1.0000 | 1.0000 + TO | 1.0000 | 1.0000 | 1.0000 + VB | 1.0000 | 1.0000 | 1.0000 + VBD | 0.8571 | 0.8571 | 0.8571 + VBG | 1.0000 | 0.8000 | 0.8889 + VBN | 1.0000 | 0.8000 | 0.8889 + VBP | 1.0000 | 1.0000 | 1.0000 + VBZ | 1.0000 | 1.0000 | 1.0000 + WDT | 0.0000 | 0.0000 | 0.0000 + `` | 1.0000 | 1.0000 | 1.0000 + + + :param gold: The list of tagged sentences to score the tagger on. + :type gold: list(list(tuple(str, str))) + :param alpha: Ratio of the cost of false negative compared to false + positives, as used in the f-measure computation. Defaults to 0.5, + where the costs are equal. + :type alpha: float + :param truncate: If specified, then only show the specified + number of values. Any sorting (e.g., sort_by_count) + will be performed before truncation. Defaults to None + :type truncate: int, optional + :param sort_by_count: Whether to sort the outputs on number of + occurrences of that tag in the ``gold`` data, defaults to False + :type sort_by_count: bool, optional + :return: A tabulated recall, precision and f-measure string + :rtype: str + """ + cm = self.confusion(gold) + return cm.evaluate(alpha=alpha, truncate=truncate, sort_by_count=sort_by_count) + + def _check_params(self, train, model): + if (train and model) or (not train and not model): + raise ValueError("Must specify either training data or trained model.") + + +class FeaturesetTaggerI(TaggerI): + """ + A tagger that requires tokens to be ``featuresets``. A featureset + is a dictionary that maps from feature names to feature + values. See ``nltk.classify`` for more information about features + and featuresets. + """ diff --git a/nltk/tag/brill.py b/nltk/tag/brill.py new file mode 100644 index 0000000..3e05da3 --- /dev/null +++ b/nltk/tag/brill.py @@ -0,0 +1,449 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from collections import Counter, defaultdict + +from nltk import jsontags +from nltk.tag import TaggerI +from nltk.tbl import Feature, Template + +###################################################################### +# Brill Templates +###################################################################### + + +@jsontags.register_tag +class Word(Feature): + """ + Feature which examines the text (word) of nearby tokens. + """ + + json_tag = "nltk.tag.brill.Word" + + @staticmethod + def extract_property(tokens, index): + """@return: The given token's text.""" + return tokens[index][0] + + +@jsontags.register_tag +class Pos(Feature): + """ + Feature which examines the tags of nearby tokens. + """ + + json_tag = "nltk.tag.brill.Pos" + + @staticmethod + def extract_property(tokens, index): + """@return: The given token's tag.""" + return tokens[index][1] + + +def nltkdemo18(): + """ + Return 18 templates, from the original nltk demo, in multi-feature syntax + """ + return [ + Template(Pos([-1])), + Template(Pos([1])), + Template(Pos([-2])), + Template(Pos([2])), + Template(Pos([-2, -1])), + Template(Pos([1, 2])), + Template(Pos([-3, -2, -1])), + Template(Pos([1, 2, 3])), + Template(Pos([-1]), Pos([1])), + Template(Word([-1])), + Template(Word([1])), + Template(Word([-2])), + Template(Word([2])), + Template(Word([-2, -1])), + Template(Word([1, 2])), + Template(Word([-3, -2, -1])), + Template(Word([1, 2, 3])), + Template(Word([-1]), Word([1])), + ] + + +def nltkdemo18plus(): + """ + Return 18 templates, from the original nltk demo, and additionally a few + multi-feature ones (the motivation is easy comparison with nltkdemo18) + """ + return nltkdemo18() + [ + Template(Word([-1]), Pos([1])), + Template(Pos([-1]), Word([1])), + Template(Word([-1]), Word([0]), Pos([1])), + Template(Pos([-1]), Word([0]), Word([1])), + Template(Pos([-1]), Word([0]), Pos([1])), + ] + + +def fntbl37(): + """ + Return 37 templates taken from the postagging task of the + fntbl distribution https://www.cs.jhu.edu/~rflorian/fntbl/ + (37 is after excluding a handful which do not condition on Pos[0]; + fntbl can do that but the current nltk implementation cannot.) + """ + return [ + Template(Word([0]), Word([1]), Word([2])), + Template(Word([-1]), Word([0]), Word([1])), + Template(Word([0]), Word([-1])), + Template(Word([0]), Word([1])), + Template(Word([0]), Word([2])), + Template(Word([0]), Word([-2])), + Template(Word([1, 2])), + Template(Word([-2, -1])), + Template(Word([1, 2, 3])), + Template(Word([-3, -2, -1])), + Template(Word([0]), Pos([2])), + Template(Word([0]), Pos([-2])), + Template(Word([0]), Pos([1])), + Template(Word([0]), Pos([-1])), + Template(Word([0])), + Template(Word([-2])), + Template(Word([2])), + Template(Word([1])), + Template(Word([-1])), + Template(Pos([-1]), Pos([1])), + Template(Pos([1]), Pos([2])), + Template(Pos([-1]), Pos([-2])), + Template(Pos([1])), + Template(Pos([-1])), + Template(Pos([-2])), + Template(Pos([2])), + Template(Pos([1, 2, 3])), + Template(Pos([1, 2])), + Template(Pos([-3, -2, -1])), + Template(Pos([-2, -1])), + Template(Pos([1]), Word([0]), Word([1])), + Template(Pos([1]), Word([0]), Word([-1])), + Template(Pos([-1]), Word([-1]), Word([0])), + Template(Pos([-1]), Word([0]), Word([1])), + Template(Pos([-2]), Pos([-1])), + Template(Pos([1]), Pos([2])), + Template(Pos([1]), Pos([2]), Word([1])), + ] + + +def brill24(): + """ + Return 24 templates of the seminal TBL paper, Brill (1995) + """ + return [ + Template(Pos([-1])), + Template(Pos([1])), + Template(Pos([-2])), + Template(Pos([2])), + Template(Pos([-2, -1])), + Template(Pos([1, 2])), + Template(Pos([-3, -2, -1])), + Template(Pos([1, 2, 3])), + Template(Pos([-1]), Pos([1])), + Template(Pos([-2]), Pos([-1])), + Template(Pos([1]), Pos([2])), + Template(Word([-1])), + Template(Word([1])), + Template(Word([-2])), + Template(Word([2])), + Template(Word([-2, -1])), + Template(Word([1, 2])), + Template(Word([-1, 0])), + Template(Word([0, 1])), + Template(Word([0])), + Template(Word([-1]), Pos([-1])), + Template(Word([1]), Pos([1])), + Template(Word([0]), Word([-1]), Pos([-1])), + Template(Word([0]), Word([1]), Pos([1])), + ] + + +def describe_template_sets(): + """ + Print the available template sets in this demo, with a short description" + """ + import inspect + import sys + + # a bit of magic to get all functions in this module + templatesets = inspect.getmembers(sys.modules[__name__], inspect.isfunction) + for name, obj in templatesets: + if name == "describe_template_sets": + continue + print(name, obj.__doc__, "\n") + + +###################################################################### +# The Brill Tagger +###################################################################### + + +@jsontags.register_tag +class BrillTagger(TaggerI): + """ + Brill's transformational rule-based tagger. Brill taggers use an + initial tagger (such as ``tag.DefaultTagger``) to assign an initial + tag sequence to a text; and then apply an ordered list of + transformational rules to correct the tags of individual tokens. + These transformation rules are specified by the ``TagRule`` + interface. + + Brill taggers can be created directly, from an initial tagger and + a list of transformational rules; but more often, Brill taggers + are created by learning rules from a training corpus, using one + of the TaggerTrainers available. + """ + + json_tag = "nltk.tag.BrillTagger" + + def __init__(self, initial_tagger, rules, training_stats=None): + """ + :param initial_tagger: The initial tagger + :type initial_tagger: TaggerI + + :param rules: An ordered list of transformation rules that + should be used to correct the initial tagging. + :type rules: list(TagRule) + + :param training_stats: A dictionary of statistics collected + during training, for possible later use + :type training_stats: dict + + """ + self._initial_tagger = initial_tagger + self._rules = tuple(rules) + self._training_stats = training_stats + + def encode_json_obj(self): + return self._initial_tagger, self._rules, self._training_stats + + @classmethod + def decode_json_obj(cls, obj): + _initial_tagger, _rules, _training_stats = obj + return cls(_initial_tagger, _rules, _training_stats) + + def rules(self): + """ + Return the ordered list of transformation rules that this tagger has learnt + + :return: the ordered list of transformation rules that correct the initial tagging + :rtype: list of Rules + """ + return self._rules + + def train_stats(self, statistic=None): + """ + Return a named statistic collected during training, or a dictionary of all + available statistics if no name given + + :param statistic: name of statistic + :type statistic: str + :return: some statistic collected during training of this tagger + :rtype: any (but usually a number) + """ + if statistic is None: + return self._training_stats + else: + return self._training_stats.get(statistic) + + def tag(self, tokens): + # Inherit documentation from TaggerI + + # Run the initial tagger. + tagged_tokens = self._initial_tagger.tag(tokens) + + # Create a dictionary that maps each tag to a list of the + # indices of tokens that have that tag. + tag_to_positions = defaultdict(set) + for i, (token, tag) in enumerate(tagged_tokens): + tag_to_positions[tag].add(i) + + # Apply each rule, in order. Only try to apply rules at + # positions that have the desired original tag. + for rule in self._rules: + # Find the positions where it might apply + positions = tag_to_positions.get(rule.original_tag, []) + # Apply the rule at those positions. + changed = rule.apply(tagged_tokens, positions) + # Update tag_to_positions with the positions of tags that + # were modified. + for i in changed: + tag_to_positions[rule.original_tag].remove(i) + tag_to_positions[rule.replacement_tag].add(i) + + return tagged_tokens + + def print_template_statistics(self, test_stats=None, printunused=True): + """ + Print a list of all templates, ranked according to efficiency. + + If test_stats is available, the templates are ranked according to their + relative contribution (summed for all rules created from a given template, + weighted by score) to the performance on the test set. If no test_stats, then + statistics collected during training are used instead. There is also + an unweighted measure (just counting the rules). This is less informative, + though, as many low-score rules will appear towards end of training. + + :param test_stats: dictionary of statistics collected during testing + :type test_stats: dict of str -> any (but usually numbers) + :param printunused: if True, print a list of all unused templates + :type printunused: bool + :return: None + :rtype: None + """ + tids = [r.templateid for r in self._rules] + train_stats = self.train_stats() + + trainscores = train_stats["rulescores"] + assert len(trainscores) == len( + tids + ), "corrupt statistics: " "{} train scores for {} rules".format( + trainscores, tids + ) + template_counts = Counter(tids) + weighted_traincounts = Counter() + for tid, score in zip(tids, trainscores): + weighted_traincounts[tid] += score + tottrainscores = sum(trainscores) + + # det_tplsort() is for deterministic sorting; + # the otherwise convenient Counter.most_common() unfortunately + # does not break ties deterministically + # between python versions and will break cross-version tests + def det_tplsort(tpl_value): + return (tpl_value[1], repr(tpl_value[0])) + + def print_train_stats(): + print( + "TEMPLATE STATISTICS (TRAIN) {} templates, {} rules)".format( + len(template_counts), len(tids) + ) + ) + print( + "TRAIN ({tokencount:7d} tokens) initial {initialerrors:5d} {initialacc:.4f} " + "final: {finalerrors:5d} {finalacc:.4f}".format(**train_stats) + ) + head = "#ID | Score (train) | #Rules | Template" + print(head, "\n", "-" * len(head), sep="") + train_tplscores = sorted( + weighted_traincounts.items(), key=det_tplsort, reverse=True + ) + for tid, trainscore in train_tplscores: + s = "{} | {:5d} {:5.3f} |{:4d} {:.3f} | {}".format( + tid, + trainscore, + trainscore / tottrainscores, + template_counts[tid], + template_counts[tid] / len(tids), + Template.ALLTEMPLATES[int(tid)], + ) + print(s) + + def print_testtrain_stats(): + testscores = test_stats["rulescores"] + print( + "TEMPLATE STATISTICS (TEST AND TRAIN) ({} templates, {} rules)".format( + len(template_counts), len(tids) + ) + ) + print( + "TEST ({tokencount:7d} tokens) initial {initialerrors:5d} {initialacc:.4f} " + "final: {finalerrors:5d} {finalacc:.4f} ".format(**test_stats) + ) + print( + "TRAIN ({tokencount:7d} tokens) initial {initialerrors:5d} {initialacc:.4f} " + "final: {finalerrors:5d} {finalacc:.4f} ".format(**train_stats) + ) + weighted_testcounts = Counter() + for tid, score in zip(tids, testscores): + weighted_testcounts[tid] += score + tottestscores = sum(testscores) + head = "#ID | Score (test) | Score (train) | #Rules | Template" + print(head, "\n", "-" * len(head), sep="") + test_tplscores = sorted( + weighted_testcounts.items(), key=det_tplsort, reverse=True + ) + for tid, testscore in test_tplscores: + s = "{:s} |{:5d} {:6.3f} | {:4d} {:.3f} |{:4d} {:.3f} | {:s}".format( + tid, + testscore, + testscore / tottestscores, + weighted_traincounts[tid], + weighted_traincounts[tid] / tottrainscores, + template_counts[tid], + template_counts[tid] / len(tids), + Template.ALLTEMPLATES[int(tid)], + ) + print(s) + + def print_unused_templates(): + usedtpls = {int(tid) for tid in tids} + unused = [ + (tid, tpl) + for (tid, tpl) in enumerate(Template.ALLTEMPLATES) + if tid not in usedtpls + ] + print(f"UNUSED TEMPLATES ({len(unused)})") + + for tid, tpl in unused: + print(f"{tid:03d} {str(tpl):s}") + + if test_stats is None: + print_train_stats() + else: + print_testtrain_stats() + print() + if printunused: + print_unused_templates() + print() + + def batch_tag_incremental(self, sequences, gold): + """ + Tags by applying each rule to the entire corpus (rather than all rules to a + single sequence). The point is to collect statistics on the test set for + individual rules. + + NOTE: This is inefficient (does not build any index, so will traverse the entire + corpus N times for N rules) -- usually you would not care about statistics for + individual rules and thus use batch_tag() instead + + :param sequences: lists of token sequences (sentences, in some applications) to be tagged + :type sequences: list of list of strings + :param gold: the gold standard + :type gold: list of list of strings + :returns: tuple of (tagged_sequences, ordered list of rule scores (one for each rule)) + """ + + def counterrors(xs): + return sum(t[1] != g[1] for pair in zip(xs, gold) for (t, g) in zip(*pair)) + + testing_stats = {} + testing_stats["tokencount"] = sum(len(t) for t in sequences) + testing_stats["sequencecount"] = len(sequences) + tagged_tokenses = [self._initial_tagger.tag(tokens) for tokens in sequences] + testing_stats["initialerrors"] = counterrors(tagged_tokenses) + testing_stats["initialacc"] = ( + 1 - testing_stats["initialerrors"] / testing_stats["tokencount"] + ) + # Apply each rule to the entire corpus, in order + errors = [testing_stats["initialerrors"]] + for rule in self._rules: + for tagged_tokens in tagged_tokenses: + rule.apply(tagged_tokens) + errors.append(counterrors(tagged_tokenses)) + testing_stats["rulescores"] = [ + err0 - err1 for (err0, err1) in zip(errors, errors[1:]) + ] + testing_stats["finalerrors"] = errors[-1] + testing_stats["finalacc"] = ( + 1 - testing_stats["finalerrors"] / testing_stats["tokencount"] + ) + return (tagged_tokenses, testing_stats) diff --git a/nltk/tag/brill_trainer.py b/nltk/tag/brill_trainer.py new file mode 100644 index 0000000..cfcf228 --- /dev/null +++ b/nltk/tag/brill_trainer.py @@ -0,0 +1,628 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2013 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +import bisect +import textwrap +from collections import defaultdict + +from nltk.tag import BrillTagger, untag + +###################################################################### +# Brill Tagger Trainer +###################################################################### + + +class BrillTaggerTrainer: + """ + A trainer for tbl taggers. + """ + + def __init__( + self, initial_tagger, templates, trace=0, deterministic=None, ruleformat="str" + ): + """ + Construct a Brill tagger from a baseline tagger and a + set of templates + + :param initial_tagger: the baseline tagger + :type initial_tagger: Tagger + :param templates: templates to be used in training + :type templates: list of Templates + :param trace: verbosity level + :type trace: int + :param deterministic: if True, adjudicate ties deterministically + :type deterministic: bool + :param ruleformat: format of reported Rules + :type ruleformat: str + :return: An untrained BrillTagger + :rtype: BrillTagger + """ + + if deterministic is None: + deterministic = trace > 0 + self._initial_tagger = initial_tagger + self._templates = templates + self._trace = trace + self._deterministic = deterministic + self._ruleformat = ruleformat + + self._tag_positions = None + """Mapping from tags to lists of positions that use that tag.""" + + self._rules_by_position = None + """Mapping from positions to the set of rules that are known + to occur at that position. Position is (sentnum, wordnum). + Initially, this will only contain positions where each rule + applies in a helpful way; but when we examine a rule, we'll + extend this list to also include positions where each rule + applies in a harmful or neutral way.""" + + self._positions_by_rule = None + """Mapping from rule to position to effect, specifying the + effect that each rule has on the overall score, at each + position. Position is (sentnum, wordnum); and effect is + -1, 0, or 1. As with _rules_by_position, this mapping starts + out only containing rules with positive effects; but when + we examine a rule, we'll extend this mapping to include + the positions where the rule is harmful or neutral.""" + + self._rules_by_score = None + """Mapping from scores to the set of rules whose effect on the + overall score is upper bounded by that score. Invariant: + rulesByScore[s] will contain r iff the sum of + _positions_by_rule[r] is s.""" + + self._rule_scores = None + """Mapping from rules to upper bounds on their effects on the + overall score. This is the inverse mapping to _rules_by_score. + Invariant: ruleScores[r] = sum(_positions_by_rule[r])""" + + self._first_unknown_position = None + """Mapping from rules to the first position where we're unsure + if the rule applies. This records the next position we + need to check to see if the rule messed anything up.""" + + # Training + + def train(self, train_sents, max_rules=200, min_score=2, min_acc=None): + r""" + Trains the Brill tagger on the corpus *train_sents*, + producing at most *max_rules* transformations, each of which + reduces the net number of errors in the corpus by at least + *min_score*, and each of which has accuracy not lower than + *min_acc*. + + >>> # Relevant imports + >>> from nltk.tbl.template import Template + >>> from nltk.tag.brill import Pos, Word + >>> from nltk.tag import untag, RegexpTagger, BrillTaggerTrainer + + >>> # Load some data + >>> from nltk.corpus import treebank + >>> training_data = treebank.tagged_sents()[:100] + >>> baseline_data = treebank.tagged_sents()[100:200] + >>> gold_data = treebank.tagged_sents()[200:300] + >>> testing_data = [untag(s) for s in gold_data] + + >>> backoff = RegexpTagger([ + ... (r'^-?[0-9]+(\.[0-9]+)?$', 'CD'), # cardinal numbers + ... (r'(The|the|A|a|An|an)$', 'AT'), # articles + ... (r'.*able$', 'JJ'), # adjectives + ... (r'.*ness$', 'NN'), # nouns formed from adjectives + ... (r'.*ly$', 'RB'), # adverbs + ... (r'.*s$', 'NNS'), # plural nouns + ... (r'.*ing$', 'VBG'), # gerunds + ... (r'.*ed$', 'VBD'), # past tense verbs + ... (r'.*', 'NN') # nouns (default) + ... ]) + + >>> baseline = backoff #see NOTE1 + >>> baseline.accuracy(gold_data) #doctest: +ELLIPSIS + 0.243... + + >>> # Set up templates + >>> Template._cleartemplates() #clear any templates created in earlier tests + >>> templates = [Template(Pos([-1])), Template(Pos([-1]), Word([0]))] + + >>> # Construct a BrillTaggerTrainer + >>> tt = BrillTaggerTrainer(baseline, templates, trace=3) + + >>> tagger1 = tt.train(training_data, max_rules=10) + TBL train (fast) (seqs: 100; tokens: 2417; tpls: 2; min score: 2; min acc: None) + Finding initial useful rules... + Found 847 useful rules. + + B | + S F r O | Score = Fixed - Broken + c i o t | R Fixed = num tags changed incorrect -> correct + o x k h | u Broken = num tags changed correct -> incorrect + r e e e | l Other = num tags changed incorrect -> incorrect + e d n r | e + ------------------+------------------------------------------------------- + 132 132 0 0 | AT->DT if Pos:NN@[-1] + 85 85 0 0 | NN->, if Pos:NN@[-1] & Word:,@[0] + 69 69 0 0 | NN->. if Pos:NN@[-1] & Word:.@[0] + 51 51 0 0 | NN->IN if Pos:NN@[-1] & Word:of@[0] + 47 63 16 162 | NN->IN if Pos:NNS@[-1] + 33 33 0 0 | NN->TO if Pos:NN@[-1] & Word:to@[0] + 26 26 0 0 | IN->. if Pos:NNS@[-1] & Word:.@[0] + 24 24 0 0 | IN->, if Pos:NNS@[-1] & Word:,@[0] + 22 27 5 24 | NN->-NONE- if Pos:VBD@[-1] + 17 17 0 0 | NN->CC if Pos:NN@[-1] & Word:and@[0] + + >>> tagger1.rules()[1:3] + (Rule('001', 'NN', ',', [(Pos([-1]),'NN'), (Word([0]),',')]), Rule('001', 'NN', '.', [(Pos([-1]),'NN'), (Word([0]),'.')])) + + >>> train_stats = tagger1.train_stats() + >>> [train_stats[stat] for stat in ['initialerrors', 'finalerrors', 'rulescores']] + [1776, 1270, [132, 85, 69, 51, 47, 33, 26, 24, 22, 17]] + + >>> tagger1.print_template_statistics(printunused=False) + TEMPLATE STATISTICS (TRAIN) 2 templates, 10 rules) + TRAIN ( 2417 tokens) initial 1776 0.2652 final: 1270 0.4746 + #ID | Score (train) | #Rules | Template + -------------------------------------------- + 001 | 305 0.603 | 7 0.700 | Template(Pos([-1]),Word([0])) + 000 | 201 0.397 | 3 0.300 | Template(Pos([-1])) + + + + >>> round(tagger1.accuracy(gold_data),5) + 0.43834 + + >>> tagged, test_stats = tagger1.batch_tag_incremental(testing_data, gold_data) + + >>> tagged[33][12:] == [('foreign', 'IN'), ('debt', 'NN'), ('of', 'IN'), ('$', 'NN'), ('64', 'CD'), + ... ('billion', 'NN'), ('*U*', 'NN'), ('--', 'NN'), ('the', 'DT'), ('third-highest', 'NN'), ('in', 'NN'), + ... ('the', 'DT'), ('developing', 'VBG'), ('world', 'NN'), ('.', '.')] + True + + >>> [test_stats[stat] for stat in ['initialerrors', 'finalerrors', 'rulescores']] + [1859, 1380, [100, 85, 67, 58, 27, 36, 27, 16, 31, 32]] + + >>> # A high-accuracy tagger + >>> tagger2 = tt.train(training_data, max_rules=10, min_acc=0.99) + TBL train (fast) (seqs: 100; tokens: 2417; tpls: 2; min score: 2; min acc: 0.99) + Finding initial useful rules... + Found 847 useful rules. + + B | + S F r O | Score = Fixed - Broken + c i o t | R Fixed = num tags changed incorrect -> correct + o x k h | u Broken = num tags changed correct -> incorrect + r e e e | l Other = num tags changed incorrect -> incorrect + e d n r | e + ------------------+------------------------------------------------------- + 132 132 0 0 | AT->DT if Pos:NN@[-1] + 85 85 0 0 | NN->, if Pos:NN@[-1] & Word:,@[0] + 69 69 0 0 | NN->. if Pos:NN@[-1] & Word:.@[0] + 51 51 0 0 | NN->IN if Pos:NN@[-1] & Word:of@[0] + 36 36 0 0 | NN->TO if Pos:NN@[-1] & Word:to@[0] + 26 26 0 0 | NN->. if Pos:NNS@[-1] & Word:.@[0] + 24 24 0 0 | NN->, if Pos:NNS@[-1] & Word:,@[0] + 19 19 0 6 | NN->VB if Pos:TO@[-1] + 18 18 0 0 | CD->-NONE- if Pos:NN@[-1] & Word:0@[0] + 18 18 0 0 | NN->CC if Pos:NN@[-1] & Word:and@[0] + + >>> round(tagger2.accuracy(gold_data), 8) + 0.43996744 + + >>> tagger2.rules()[2:4] + (Rule('001', 'NN', '.', [(Pos([-1]),'NN'), (Word([0]),'.')]), Rule('001', 'NN', 'IN', [(Pos([-1]),'NN'), (Word([0]),'of')])) + + # NOTE1: (!!FIXME) A far better baseline uses nltk.tag.UnigramTagger, + # with a RegexpTagger only as backoff. For instance, + # >>> baseline = UnigramTagger(baseline_data, backoff=backoff) + # However, as of Nov 2013, nltk.tag.UnigramTagger does not yield consistent results + # between python versions. The simplistic backoff above is a workaround to make doctests + # get consistent input. + + :param train_sents: training data + :type train_sents: list(list(tuple)) + :param max_rules: output at most max_rules rules + :type max_rules: int + :param min_score: stop training when no rules better than min_score can be found + :type min_score: int + :param min_acc: discard any rule with lower accuracy than min_acc + :type min_acc: float or None + :return: the learned tagger + :rtype: BrillTagger + """ + # FIXME: several tests are a bit too dependent on tracing format + # FIXME: tests in trainer.fast and trainer.brillorig are exact duplicates + + # Basic idea: Keep track of the rules that apply at each position. + # And keep track of the positions to which each rule applies. + + # Create a new copy of the training corpus, and run the + # initial tagger on it. We will progressively update this + # test corpus to look more like the training corpus. + test_sents = [ + list(self._initial_tagger.tag(untag(sent))) for sent in train_sents + ] + + # Collect some statistics on the training process + trainstats = {} + trainstats["min_acc"] = min_acc + trainstats["min_score"] = min_score + trainstats["tokencount"] = sum(len(t) for t in test_sents) + trainstats["sequencecount"] = len(test_sents) + trainstats["templatecount"] = len(self._templates) + trainstats["rulescores"] = [] + trainstats["initialerrors"] = sum( + tag[1] != truth[1] + for paired in zip(test_sents, train_sents) + for (tag, truth) in zip(*paired) + ) + trainstats["initialacc"] = ( + 1 - trainstats["initialerrors"] / trainstats["tokencount"] + ) + if self._trace > 0: + print( + "TBL train (fast) (seqs: {sequencecount}; tokens: {tokencount}; " + "tpls: {templatecount}; min score: {min_score}; min acc: {min_acc})".format( + **trainstats + ) + ) + + # Initialize our mappings. This will find any errors made + # by the initial tagger, and use those to generate repair + # rules, which are added to the rule mappings. + if self._trace: + print("Finding initial useful rules...") + self._init_mappings(test_sents, train_sents) + if self._trace: + print(f" Found {len(self._rule_scores)} useful rules.") + + # Let the user know what we're up to. + if self._trace > 2: + self._trace_header() + elif self._trace == 1: + print("Selecting rules...") + + # Repeatedly select the best rule, and add it to `rules`. + rules = [] + try: + while len(rules) < max_rules: + # Find the best rule, and add it to our rule list. + rule = self._best_rule(train_sents, test_sents, min_score, min_acc) + if rule: + rules.append(rule) + score = self._rule_scores[rule] + trainstats["rulescores"].append(score) + else: + break # No more good rules left! + + # Report the rule that we found. + if self._trace > 1: + self._trace_rule(rule) + + # Apply the new rule at the relevant sites + self._apply_rule(rule, test_sents) + + # Update _tag_positions[rule.original_tag] and + # _tag_positions[rule.replacement_tag] for the affected + # positions (i.e., self._positions_by_rule[rule]). + self._update_tag_positions(rule) + + # Update rules that were affected by the change. + self._update_rules(rule, train_sents, test_sents) + + # The user can cancel training manually: + except KeyboardInterrupt: + print(f"Training stopped manually -- {len(rules)} rules found") + + # Discard our tag position mapping & rule mappings. + self._clean() + trainstats["finalerrors"] = trainstats["initialerrors"] - sum( + trainstats["rulescores"] + ) + trainstats["finalacc"] = ( + 1 - trainstats["finalerrors"] / trainstats["tokencount"] + ) + # Create and return a tagger from the rules we found. + return BrillTagger(self._initial_tagger, rules, trainstats) + + def _init_mappings(self, test_sents, train_sents): + """ + Initialize the tag position mapping & the rule related + mappings. For each error in test_sents, find new rules that + would correct them, and add them to the rule mappings. + """ + self._tag_positions = defaultdict(list) + self._rules_by_position = defaultdict(set) + self._positions_by_rule = defaultdict(dict) + self._rules_by_score = defaultdict(set) + self._rule_scores = defaultdict(int) + self._first_unknown_position = defaultdict(int) + # Scan through the corpus, initializing the tag_positions + # mapping and all the rule-related mappings. + for sentnum, sent in enumerate(test_sents): + for wordnum, (word, tag) in enumerate(sent): + # Initialize tag_positions + self._tag_positions[tag].append((sentnum, wordnum)) + + # If it's an error token, update the rule-related mappings. + correct_tag = train_sents[sentnum][wordnum][1] + if tag != correct_tag: + for rule in self._find_rules(sent, wordnum, correct_tag): + self._update_rule_applies(rule, sentnum, wordnum, train_sents) + + def _clean(self): + self._tag_positions = None + self._rules_by_position = None + self._positions_by_rule = None + self._rules_by_score = None + self._rule_scores = None + self._first_unknown_position = None + + def _find_rules(self, sent, wordnum, new_tag): + """ + Use the templates to find rules that apply at index *wordnum* + in the sentence *sent* and generate the tag *new_tag*. + """ + for template in self._templates: + yield from template.applicable_rules(sent, wordnum, new_tag) + + def _update_rule_applies(self, rule, sentnum, wordnum, train_sents): + """ + Update the rule data tables to reflect the fact that + *rule* applies at the position *(sentnum, wordnum)*. + """ + pos = sentnum, wordnum + + # If the rule is already known to apply here, ignore. + # (This only happens if the position's tag hasn't changed.) + if pos in self._positions_by_rule[rule]: + return + + # Update self._positions_by_rule. + correct_tag = train_sents[sentnum][wordnum][1] + if rule.replacement_tag == correct_tag: + self._positions_by_rule[rule][pos] = 1 + elif rule.original_tag == correct_tag: + self._positions_by_rule[rule][pos] = -1 + else: # was wrong, remains wrong + self._positions_by_rule[rule][pos] = 0 + + # Update _rules_by_position + self._rules_by_position[pos].add(rule) + + # Update _rule_scores. + old_score = self._rule_scores[rule] + self._rule_scores[rule] += self._positions_by_rule[rule][pos] + + # Update _rules_by_score. + self._rules_by_score[old_score].discard(rule) + self._rules_by_score[self._rule_scores[rule]].add(rule) + + def _update_rule_not_applies(self, rule, sentnum, wordnum): + """ + Update the rule data tables to reflect the fact that *rule* + does not apply at the position *(sentnum, wordnum)*. + """ + pos = sentnum, wordnum + + # Update _rule_scores. + old_score = self._rule_scores[rule] + self._rule_scores[rule] -= self._positions_by_rule[rule][pos] + + # Update _rules_by_score. + self._rules_by_score[old_score].discard(rule) + self._rules_by_score[self._rule_scores[rule]].add(rule) + + # Update _positions_by_rule + del self._positions_by_rule[rule][pos] + self._rules_by_position[pos].remove(rule) + + # Optional addition: if the rule now applies nowhere, delete + # all its dictionary entries. + + def _best_rule(self, train_sents, test_sents, min_score, min_acc): + """ + Find the next best rule. This is done by repeatedly taking a + rule with the highest score and stepping through the corpus to + see where it applies. When it makes an error (decreasing its + score) it's bumped down, and we try a new rule with the + highest score. When we find a rule which has the highest + score *and* which has been tested against the entire corpus, we + can conclude that it's the next best rule. + """ + for max_score in sorted(self._rules_by_score.keys(), reverse=True): + if len(self._rules_by_score) == 0: + return None + if max_score < min_score or max_score <= 0: + return None + best_rules = list(self._rules_by_score[max_score]) + if self._deterministic: + best_rules.sort(key=repr) + for rule in best_rules: + positions = self._tag_positions[rule.original_tag] + + unk = self._first_unknown_position.get(rule, (0, -1)) + start = bisect.bisect_left(positions, unk) + + for i in range(start, len(positions)): + sentnum, wordnum = positions[i] + if rule.applies(test_sents[sentnum], wordnum): + self._update_rule_applies(rule, sentnum, wordnum, train_sents) + if self._rule_scores[rule] < max_score: + self._first_unknown_position[rule] = (sentnum, wordnum + 1) + break # The update demoted the rule. + + if self._rule_scores[rule] == max_score: + self._first_unknown_position[rule] = (len(train_sents) + 1, 0) + # optimization: if no min_acc threshold given, don't bother computing accuracy + if min_acc is None: + return rule + else: + changes = self._positions_by_rule[rule].values() + num_fixed = len([c for c in changes if c == 1]) + num_broken = len([c for c in changes if c == -1]) + # acc here is fixed/(fixed+broken); could also be + # fixed/(fixed+broken+other) == num_fixed/len(changes) + acc = num_fixed / (num_fixed + num_broken) + if acc >= min_acc: + return rule + # else: rule too inaccurate, discard and try next + + # We demoted (or skipped due to < min_acc, if that was given) + # all the rules with score==max_score. + + assert min_acc is not None or not self._rules_by_score[max_score] + if not self._rules_by_score[max_score]: + del self._rules_by_score[max_score] + + def _apply_rule(self, rule, test_sents): + """ + Update *test_sents* by applying *rule* everywhere where its + conditions are met. + """ + update_positions = set(self._positions_by_rule[rule]) + new_tag = rule.replacement_tag + + if self._trace > 3: + self._trace_apply(len(update_positions)) + + # Update test_sents. + for sentnum, wordnum in update_positions: + text = test_sents[sentnum][wordnum][0] + test_sents[sentnum][wordnum] = (text, new_tag) + + def _update_tag_positions(self, rule): + """ + Update _tag_positions to reflect the changes to tags that are + made by *rule*. + """ + # Update the tag index. + for pos in self._positions_by_rule[rule]: + # Delete the old tag. + old_tag_positions = self._tag_positions[rule.original_tag] + old_index = bisect.bisect_left(old_tag_positions, pos) + del old_tag_positions[old_index] + # Insert the new tag. + new_tag_positions = self._tag_positions[rule.replacement_tag] + bisect.insort_left(new_tag_positions, pos) + + def _update_rules(self, rule, train_sents, test_sents): + """ + Check if we should add or remove any rules from consideration, + given the changes made by *rule*. + """ + # Collect a list of all positions that might be affected. + neighbors = set() + for sentnum, wordnum in self._positions_by_rule[rule]: + for template in self._templates: + n = template.get_neighborhood(test_sents[sentnum], wordnum) + neighbors.update([(sentnum, i) for i in n]) + + # Update the rules at each position. + num_obsolete = num_new = num_unseen = 0 + for sentnum, wordnum in neighbors: + test_sent = test_sents[sentnum] + correct_tag = train_sents[sentnum][wordnum][1] + + # Check if the change causes any rule at this position to + # stop matching; if so, then update our rule mappings + # accordingly. + old_rules = set(self._rules_by_position[sentnum, wordnum]) + for old_rule in old_rules: + if not old_rule.applies(test_sent, wordnum): + num_obsolete += 1 + self._update_rule_not_applies(old_rule, sentnum, wordnum) + + # Check if the change causes our templates to propose any + # new rules for this position. + for template in self._templates: + for new_rule in template.applicable_rules( + test_sent, wordnum, correct_tag + ): + if new_rule not in old_rules: + num_new += 1 + if new_rule not in self._rule_scores: + num_unseen += 1 + old_rules.add(new_rule) + self._update_rule_applies( + new_rule, sentnum, wordnum, train_sents + ) + + # We may have caused other rules to match here, that are + # not proposed by our templates -- in particular, rules + # that are harmful or neutral. We therefore need to + # update any rule whose first_unknown_position is past + # this rule. + for new_rule, pos in self._first_unknown_position.items(): + if pos > (sentnum, wordnum): + if new_rule not in old_rules: + num_new += 1 + if new_rule.applies(test_sent, wordnum): + self._update_rule_applies( + new_rule, sentnum, wordnum, train_sents + ) + + if self._trace > 3: + self._trace_update_rules(num_obsolete, num_new, num_unseen) + + # Tracing + + def _trace_header(self): + print( + """ + B | + S F r O | Score = Fixed - Broken + c i o t | R Fixed = num tags changed incorrect -> correct + o x k h | u Broken = num tags changed correct -> incorrect + r e e e | l Other = num tags changed incorrect -> incorrect + e d n r | e +------------------+------------------------------------------------------- + """.rstrip() + ) + + def _trace_rule(self, rule): + assert self._rule_scores[rule] == sum(self._positions_by_rule[rule].values()) + + changes = self._positions_by_rule[rule].values() + num_fixed = len([c for c in changes if c == 1]) + num_broken = len([c for c in changes if c == -1]) + num_other = len([c for c in changes if c == 0]) + score = self._rule_scores[rule] + + rulestr = rule.format(self._ruleformat) + if self._trace > 2: + print( + "{:4d}{:4d}{:4d}{:4d} |".format( + score, num_fixed, num_broken, num_other + ), + end=" ", + ) + print( + textwrap.fill( + rulestr, + initial_indent=" " * 20, + width=79, + subsequent_indent=" " * 18 + "| ", + ).strip() + ) + else: + print(rulestr) + + def _trace_apply(self, num_updates): + prefix = " " * 18 + "|" + print(prefix) + print(prefix, f"Applying rule to {num_updates} positions.") + + def _trace_update_rules(self, num_obsolete, num_new, num_unseen): + prefix = " " * 18 + "|" + print(prefix, "Updated rule tables:") + print(prefix, (f" - {num_obsolete} rule applications removed")) + print( + prefix, + (f" - {num_new} rule applications added ({num_unseen} novel)"), + ) + print(prefix) diff --git a/nltk/tag/crf.py b/nltk/tag/crf.py new file mode 100644 index 0000000..ac878f2 --- /dev/null +++ b/nltk/tag/crf.py @@ -0,0 +1,316 @@ +# Natural Language Toolkit: Interface to the CRFSuite Tagger +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Long Duong +# John Winstead (fixes) +# URL: +# For license information, see LICENSE.TXT + +""" +A module for POS tagging using CRFSuite +""" + +import re +import unicodedata +import warnings + +from nltk.tag.api import TaggerI + +try: + import pycrfsuite +except ImportError: + pycrfsuite = None + +# Punctuation categories from the Unicode general-category table. +# Module-level so the per-token feature loop doesn't rebuild it. +_PUNC_CATEGORIES = frozenset({"Pc", "Pd", "Ps", "Pe", "Pi", "Pf", "Po"}) + + +class CRFTagger(TaggerI): + """ + A module for POS tagging using CRFSuite https://pypi.python.org/pypi/python-crfsuite + + >>> from nltk.tag import CRFTagger + >>> ct = CRFTagger() # doctest: +SKIP + + >>> train_data = [ + ... [('University','Noun'), ('is','Verb'), ('a','Det'), + ... ('good','Adj'), ('place','Noun')], + ... [('dog','Noun'), ('eat','Verb'), ('meat','Noun')], + ... ] + + >>> ct.train(train_data, 'model.crf.tagger') # doctest: +SKIP + >>> ct.tag_sents([['dog','is','good'], ['Cat','eat','meat']]) # doctest: +SKIP + [[('dog', 'Noun'), ('is', 'Verb'), ('good', 'Adj')], + [('Cat', 'Noun'), ('eat', 'Verb'), ('meat', 'Noun')]] + + >>> gold_sentences = [ + ... [('dog','Noun'), ('is','Verb'), ('good','Adj')], + ... [('Cat','Noun'), ('eat','Verb'), ('meat','Noun')], + ... ] + >>> ct.accuracy(gold_sentences) # doctest: +SKIP + 1.0 + + Setting learned model file + >>> ct = CRFTagger() # doctest: +SKIP + >>> ct.set_model_file('model.crf.tagger') # doctest: +SKIP + >>> ct.accuracy(gold_sentences) # doctest: +SKIP + 1.0 + + Default features are cached by token surface form. Call + ``clear_feature_cache()`` to drop the cache (e.g. between long-running + open-vocabulary tagging passes). A custom ``feature_func`` bypasses + the cache. + """ + + def __init__(self, feature_func=None, verbose=False, training_opt=None): + """ + Initialize the CRFSuite tagger + + :param feature_func: Function that extracts features for each token + of a sentence. Takes two parameters (``tokens``, ``idx``) and + returns the feature list for ``tokens[idx]``. See the built-in + ``_get_features`` for an example. + :param verbose: Emit debugging messages during training. + :type verbose: bool + :param training_opt: python-crfsuite training options. + :type training_opt: dict + + Set of possible training options (using LBFGS training algorithm). + :'feature.minfreq': Minimum frequency of features. + :'feature.possible_states': Force generating possible state features. + :'feature.possible_transitions': Force generating possible transition + features. + :'c1': Coefficient for L1 regularization. + :'c2': Coefficient for L2 regularization. + :'max_iterations': Maximum number of iterations for L-BFGS. + :'num_memories': Number of limited memories for approximating the + inverse Hessian matrix. + :'epsilon': Epsilon for testing the convergence of the objective. + :'period': Duration of iterations for the stopping criterion. + :'delta': Threshold for the stopping criterion; L-BFGS stops when + the log-likelihood improvement over the last ${period} + iterations is no greater than this threshold. + :'linesearch': Line search algorithm used in L-BFGS updates: + + - 'MoreThuente': More and Thuente's method, + - 'Backtracking': Backtracking method with regular Wolfe condition, + - 'StrongBacktracking': Backtracking method with strong Wolfe condition + :'max_linesearch': Maximum number of trials for the line search. + """ + + if pycrfsuite is None: + raise ImportError("CRFTagger requires python-crfsuite to be installed.") + + self._model_file = "" + self._tagger = pycrfsuite.Tagger() + + if feature_func is None: + self._feature_func = self._get_features + else: + self._feature_func = feature_func + + self._verbose = verbose + # Avoid mutable default; copy so caller mutations don't leak in. + self._training_options = {} if training_opt is None else dict(training_opt) + self._pattern = re.compile(r"\d") + # Avoid the module-level ``re.search`` dispatch in the feature loop. + self._pattern_search = self._pattern.search + # Token-keyed cache; default features are token-local. A custom + # ``feature_func`` replaces ``_get_features`` and skips this dict. + self._feature_cache = {} + + def set_model_file(self, model_file): + self._model_file = model_file + self._tagger.open(self._model_file) + + def _get_features(self, tokens, idx): + """ + Extract basic features about this word including + - Current word + - is it capitalized? + - Does it have punctuation? + - Does it have a number? + - Suffixes up to length 3 + + Note that : we might include features over previous word, next word etc. + + :return: a list which contains the features + :rtype: list(str) + """ + token = tokens[idx] + + # Return a fresh list so callers can't mutate the cached tuple. + cached = self._feature_cache.get(token) + if cached is not None: + return list(cached) + + feature_list = [] + + if not token: + self._feature_cache[token] = () + return feature_list + + append = feature_list.append + token_len = len(token) + + if token[0].isupper(): + append("CAPITALIZATION") + + if self._pattern_search(token) is not None: + append("HAS_NUM") + + category = unicodedata.category + if all(category(ch) in _PUNC_CATEGORIES for ch in token): + append("PUNCTUATION") + + if token_len > 1: + append("SUF_" + token[-1:]) + if token_len > 2: + append("SUF_" + token[-2:]) + if token_len > 3: + append("SUF_" + token[-3:]) + + append("WORD_" + token) + + self._feature_cache[token] = tuple(feature_list) + return feature_list + + def clear_feature_cache(self): + """Drop the default-feature cache. + + The cache is vocabulary-bounded, but long-running processes that + ingest open-vocabulary streams (URLs, tweets, generated text) may + want to reset it explicitly. A custom ``feature_func`` bypasses + the cache, so this is a no-op for non-default extractors. + """ + self._feature_cache.clear() + + def tag_sents(self, sentences=None, **kwargs): + """ + Tag a list of sentences. Before using this function, the model file + must be specified by either: + + - Train a new model using ``train`` function + - Use the pre-trained model which is set via ``set_model_file`` function + + :param sentences: sentences to tag. + :type sentences: list(list(str)) + :param sents: deprecated alias for ``sentences``; emits ``DeprecationWarning``. + :return: list of tagged sentences. + :rtype: list(list(tuple(str,str))) + """ + if "sents" in kwargs: + if sentences is not None: + raise TypeError( + "tag_sents() got both 'sentences' and 'sents'; " + "use 'sentences' only ('sents' is deprecated)." + ) + warnings.warn( + "tag_sents(sents=...) is deprecated; use tag_sents(sentences=...).", + DeprecationWarning, + stacklevel=2, + ) + sentences = kwargs.pop("sents") + if kwargs: + unexpected = ", ".join(sorted(kwargs)) + raise TypeError( + f"tag_sents() got unexpected keyword arguments: {unexpected}" + ) + if sentences is None: + raise TypeError("tag_sents() missing 1 required argument: 'sentences'") + + if isinstance(sentences, (str, bytes)): + raise TypeError( + "tag_sents() expects a list of tokenized sentences, " + "not a single string or bytes object." + ) + + # Catch ``[token, token, ...]`` (one tokenized sentence) before the + # model-file check so the error is clear even on an untrained tagger. + # Generators fall through to the per-iteration check below. + if ( + isinstance(sentences, (list, tuple)) + and sentences + and isinstance(sentences[0], (str, bytes)) + ): + raise TypeError( + "tag_sents() expects a list of tokenized sentences, " + "not a single tokenized sentence." + ) + + if self._model_file == "": + raise RuntimeError( + "No model file set; call train() or set_model_file() first." + ) + + # Hoist hot-loop attribute lookups out of the per-sentence loop. + feature_func = self._feature_func + tag = self._tagger.tag + + result = [] + for tokens in sentences: + if isinstance(tokens, (str, bytes)): + # Safety net for generator inputs the up-front check skipped. + raise TypeError( + "tag_sents() expects a list of tokenized sentences, " + "not a single tokenized sentence." + ) + features = [feature_func(tokens, i) for i in range(len(tokens))] + labels = tag(features) + + if len(labels) != len(tokens): + raise RuntimeError( + f"CRF returned {len(labels)} labels for {len(tokens)} tokens." + ) + + tagged_sent = list(zip(tokens, labels, strict=True)) + result.append(tagged_sent) + + return result + + def train(self, train_data, model_file): + """ + Train the CRF tagger using CRFSuite + + :param train_data: list of annotated sentences. + :type train_data: list(list(tuple(str,str))) + :param model_file: path where the trained model will be written. + :type model_file: str + """ + if pycrfsuite is None: + raise ImportError("CRFTagger requires python-crfsuite to be installed.") + + trainer = pycrfsuite.Trainer(verbose=self._verbose) + trainer.set_params(self._training_options) + + feature_func = self._feature_func + append = trainer.append + + for sent in train_data: + tokens, labels = zip(*sent, strict=True) + features = [feature_func(tokens, i) for i in range(len(tokens))] + append(features, labels) + + trainer.train(model_file) + self.set_model_file(model_file) + + def tag(self, tokens): + """ + Tag a sentence using the python-crfsuite tagger. Before using this + function, the model file must be specified by either: + + - Train a new model using ``train`` function + - Use the pre-trained model which is set via ``set_model_file`` function + + :param tokens: tokens to tag. + :type tokens: list(str) + :return: list of tagged tokens. + :rtype: list(tuple(str,str)) + """ + if isinstance(tokens, (str, bytes)): + raise TypeError( + "tag() expects a list of tokens, not a single string or bytes. " + "Tokenize first (e.g. tokens.split() or word_tokenize(s))." + ) + return self.tag_sents([tokens])[0] diff --git a/nltk/tag/hmm.py b/nltk/tag/hmm.py new file mode 100644 index 0000000..5748896 --- /dev/null +++ b/nltk/tag/hmm.py @@ -0,0 +1,1326 @@ +# Natural Language Toolkit: Hidden Markov Model +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Trevor Cohn +# Philip Blunsom +# Tiago Tresoldi (fixes) +# Steven Bird (fixes) +# Joseph Frazee (fixes) +# Steven Xu (fixes) +# URL: +# For license information, see LICENSE.TXT + +""" +Hidden Markov Models (HMMs) largely used to assign the correct label sequence +to sequential data or assess the probability of a given label and data +sequence. These models are finite state machines characterised by a number of +states, transitions between these states, and output symbols emitted while in +each state. The HMM is an extension to the Markov chain, where each state +corresponds deterministically to a given event. In the HMM the observation is +a probabilistic function of the state. HMMs share the Markov chain's +assumption, being that the probability of transition from one state to another +only depends on the current state - i.e. the series of states that led to the +current state are not used. They are also time invariant. + +The HMM is a directed graph, with probability weighted edges (representing the +probability of a transition between the source and sink states) where each +vertex emits an output symbol when entered. The symbol (or observation) is +non-deterministically generated. For this reason, knowing that a sequence of +output observations was generated by a given HMM does not mean that the +corresponding sequence of states (and what the current state is) is known. +This is the 'hidden' in the hidden markov model. + +Formally, a HMM can be characterised by: + +- the output observation alphabet. This is the set of symbols which may be + observed as output of the system. +- the set of states. +- the transition probabilities *a_{ij} = P(s_t = j | s_{t-1} = i)*. These + represent the probability of transition to each state from a given state. +- the output probability matrix *b_i(k) = P(X_t = o_k | s_t = i)*. These + represent the probability of observing each symbol in a given state. +- the initial state distribution. This gives the probability of starting + in each state. + +To ground this discussion, take a common NLP application, part-of-speech (POS) +tagging. An HMM is desirable for this task as the highest probability tag +sequence can be calculated for a given sequence of word forms. This differs +from other tagging techniques which often tag each word individually, seeking +to optimise each individual tagging greedily without regard to the optimal +combination of tags for a larger unit, such as a sentence. The HMM does this +with the Viterbi algorithm, which efficiently computes the optimal path +through the graph given the sequence of words forms. + +In POS tagging the states usually have a 1:1 correspondence with the tag +alphabet - i.e. each state represents a single tag. The output observation +alphabet is the set of word forms (the lexicon), and the remaining three +parameters are derived by a training regime. With this information the +probability of a given sentence can be easily derived, by simply summing the +probability of each distinct path through the model. Similarly, the highest +probability tagging sequence can be derived with the Viterbi algorithm, +yielding a state sequence which can be mapped into a tag sequence. + +This discussion assumes that the HMM has been trained. This is probably the +most difficult task with the model, and requires either MLE estimates of the +parameters or unsupervised learning using the Baum-Welch algorithm, a variant +of EM. + +For more information, please consult the source code for this module, +which includes extensive demonstration code. +""" + +import itertools +import re + +try: + import numpy as np +except ImportError: + pass + +from nltk.metrics import accuracy +from nltk.probability import ( + ConditionalFreqDist, + ConditionalProbDist, + DictionaryConditionalProbDist, + DictionaryProbDist, + FreqDist, + LidstoneProbDist, + MLEProbDist, + MutableProbDist, + RandomProbDist, +) +from nltk.tag.api import TaggerI +from nltk.util import LazyMap, unique_list + +_TEXT = 0 # index of text in a tuple +_TAG = 1 # index of tag in a tuple + + +def _identity(labeled_symbols): + return labeled_symbols + + +class HiddenMarkovModelTagger(TaggerI): + """ + Hidden Markov model class, a generative model for labelling sequence data. + These models define the joint probability of a sequence of symbols and + their labels (state transitions) as the product of the starting state + probability, the probability of each state transition, and the probability + of each observation being generated from each state. This is described in + more detail in the module documentation. + + This implementation is based on the HMM description in Chapter 8, Huang, + Acero and Hon, Spoken Language Processing and includes an extension for + training shallow HMM parsers or specialized HMMs as in Molina et. + al, 2002. A specialized HMM modifies training data by applying a + specialization function to create a new training set that is more + appropriate for sequential tagging with an HMM. A typical use case is + chunking. + + :param symbols: the set of output symbols (alphabet) + :type symbols: seq of any + :param states: a set of states representing state space + :type states: seq of any + :param transitions: transition probabilities; Pr(s_i | s_j) is the + probability of transition from state i given the model is in + state_j + :type transitions: ConditionalProbDistI + :param outputs: output probabilities; Pr(o_k | s_i) is the probability + of emitting symbol k when entering state i + :type outputs: ConditionalProbDistI + :param priors: initial state distribution; Pr(s_i) is the probability + of starting in state i + :type priors: ProbDistI + :param transform: an optional function for transforming training + instances, defaults to the identity function. + :type transform: callable + """ + + def __init__( + self, symbols, states, transitions, outputs, priors, transform=_identity + ): + self._symbols = unique_list(symbols) + self._states = unique_list(states) + self._transitions = transitions + self._outputs = outputs + self._priors = priors + self._cache = None + self._transform = transform + + @classmethod + def _train( + cls, + labeled_sequence, + test_sequence=None, + unlabeled_sequence=None, + transform=_identity, + estimator=None, + **kwargs, + ): + if estimator is None: + + def estimator(fd, bins): + return LidstoneProbDist(fd, 0.1, bins) + + labeled_sequence = LazyMap(transform, labeled_sequence) + symbols = unique_list(word for sent in labeled_sequence for word, tag in sent) + tag_set = unique_list(tag for sent in labeled_sequence for word, tag in sent) + + trainer = HiddenMarkovModelTrainer(tag_set, symbols) + hmm = trainer.train_supervised(labeled_sequence, estimator=estimator) + hmm = cls( + hmm._symbols, + hmm._states, + hmm._transitions, + hmm._outputs, + hmm._priors, + transform=transform, + ) + + if test_sequence: + hmm.test(test_sequence, verbose=kwargs.get("verbose", False)) + + if unlabeled_sequence: + max_iterations = kwargs.get("max_iterations", 5) + hmm = trainer.train_unsupervised( + unlabeled_sequence, model=hmm, max_iterations=max_iterations + ) + if test_sequence: + hmm.test(test_sequence, verbose=kwargs.get("verbose", False)) + + return hmm + + @classmethod + def train( + cls, labeled_sequence, test_sequence=None, unlabeled_sequence=None, **kwargs + ): + """ + Train a new HiddenMarkovModelTagger using the given labeled and + unlabeled training instances. Testing will be performed if test + instances are provided. + + :return: a hidden markov model tagger + :rtype: HiddenMarkovModelTagger + :param labeled_sequence: a sequence of labeled training instances, + i.e. a list of sentences represented as tuples + :type labeled_sequence: list(list) + :param test_sequence: a sequence of labeled test instances + :type test_sequence: list(list) + :param unlabeled_sequence: a sequence of unlabeled training instances, + i.e. a list of sentences represented as words + :type unlabeled_sequence: list(list) + :param transform: an optional function for transforming training + instances, defaults to the identity function, see ``transform()`` + :type transform: function + :param estimator: an optional function or class that maps a + condition's frequency distribution to its probability + distribution, defaults to a Lidstone distribution with gamma = 0.1 + :type estimator: class or function + :param verbose: boolean flag indicating whether training should be + verbose or include printed output + :type verbose: bool + :param max_iterations: number of Baum-Welch iterations to perform + :type max_iterations: int + """ + return cls._train(labeled_sequence, test_sequence, unlabeled_sequence, **kwargs) + + def probability(self, sequence): + """ + Returns the probability of the given symbol sequence. If the sequence + is labelled, then returns the joint probability of the symbol, state + sequence. Otherwise, uses the forward algorithm to find the + probability over all label sequences. + + :return: the probability of the sequence + :rtype: float + :param sequence: the sequence of symbols which must contain the TEXT + property, and optionally the TAG property + :type sequence: Token + """ + return 2 ** (self.log_probability(self._transform(sequence))) + + def log_probability(self, sequence): + """ + Returns the log-probability of the given symbol sequence. If the + sequence is labelled, then returns the joint log-probability of the + symbol, state sequence. Otherwise, uses the forward algorithm to find + the log-probability over all label sequences. + + :return: the log-probability of the sequence + :rtype: float + :param sequence: the sequence of symbols which must contain the TEXT + property, and optionally the TAG property + :type sequence: Token + """ + sequence = self._transform(sequence) + + T = len(sequence) + + if T > 0 and sequence[0][_TAG]: + last_state = sequence[0][_TAG] + p = self._priors.logprob(last_state) + self._output_logprob( + last_state, sequence[0][_TEXT] + ) + for t in range(1, T): + state = sequence[t][_TAG] + p += self._transitions[last_state].logprob( + state + ) + self._output_logprob(state, sequence[t][_TEXT]) + last_state = state + return p + else: + alpha = self._forward_probability(sequence) + p = logsumexp2(alpha[T - 1]) + return p + + def tag(self, unlabeled_sequence): + """ + Tags the sequence with the highest probability state sequence. This + uses the best_path method to find the Viterbi path. + + :return: a labelled sequence of symbols + :rtype: list + :param unlabeled_sequence: the sequence of unlabeled symbols + :type unlabeled_sequence: list + """ + unlabeled_sequence = self._transform(unlabeled_sequence) + return self._tag(unlabeled_sequence) + + def _tag(self, unlabeled_sequence): + path = self._best_path(unlabeled_sequence) + return list(zip(unlabeled_sequence, path)) + + def _output_logprob(self, state, symbol): + """ + :return: the log probability of the symbol being observed in the given + state + :rtype: float + """ + return self._outputs[state].logprob(symbol) + + def _create_cache(self): + """ + The cache is a tuple (P, O, X, S) where: + + - S maps symbols to integers. I.e., it is the inverse + mapping from self._symbols; for each symbol s in + self._symbols, the following is true:: + + self._symbols[S[s]] == s + + - O is the log output probabilities:: + + O[i,k] = log( P(token[t]=sym[k]|tag[t]=state[i]) ) + + - X is the log transition probabilities:: + + X[i,j] = log( P(tag[t]=state[j]|tag[t-1]=state[i]) ) + + - P is the log prior probabilities:: + + P[i] = log( P(tag[0]=state[i]) ) + """ + if not self._cache: + N = len(self._states) + M = len(self._symbols) + P = np.zeros(N, np.float32) + X = np.zeros((N, N), np.float32) + O = np.zeros((N, M), np.float32) + for i in range(N): + si = self._states[i] + P[i] = self._priors.logprob(si) + for j in range(N): + X[i, j] = self._transitions[si].logprob(self._states[j]) + for k in range(M): + O[i, k] = self._output_logprob(si, self._symbols[k]) + S = {} + for k in range(M): + S[self._symbols[k]] = k + self._cache = (P, O, X, S) + + def _update_cache(self, symbols): + # add new symbols to the symbol table and repopulate the output + # probabilities and symbol table mapping + if symbols: + self._create_cache() + P, O, X, S = self._cache + for symbol in symbols: + if symbol not in self._symbols: + self._cache = None + self._symbols.append(symbol) + # don't bother with the work if there aren't any new symbols + if not self._cache: + N = len(self._states) + M = len(self._symbols) + Q = O.shape[1] + # add new columns to the output probability table without + # destroying the old probabilities + O = np.hstack([O, np.zeros((N, M - Q), np.float32)]) + for i in range(N): + si = self._states[i] + # only calculate probabilities for new symbols + for k in range(Q, M): + O[i, k] = self._output_logprob(si, self._symbols[k]) + # only create symbol mappings for new symbols + for k in range(Q, M): + S[self._symbols[k]] = k + self._cache = (P, O, X, S) + + def reset_cache(self): + self._cache = None + + def best_path(self, unlabeled_sequence): + """ + Returns the state sequence of the optimal (most probable) path through + the HMM. Uses the Viterbi algorithm to calculate this part by dynamic + programming. + + :return: the state sequence + :rtype: sequence of any + :param unlabeled_sequence: the sequence of unlabeled symbols + :type unlabeled_sequence: list + """ + unlabeled_sequence = self._transform(unlabeled_sequence) + return self._best_path(unlabeled_sequence) + + def _best_path(self, unlabeled_sequence): + T = len(unlabeled_sequence) + N = len(self._states) + self._create_cache() + self._update_cache(unlabeled_sequence) + P, O, X, S = self._cache + + V = np.zeros((T, N), np.float32) + B = -np.ones((T, N), int) + + V[0] = P + O[:, S[unlabeled_sequence[0]]] + for t in range(1, T): + for j in range(N): + vs = V[t - 1, :] + X[:, j] + best = np.argmax(vs) + V[t, j] = vs[best] + O[j, S[unlabeled_sequence[t]]] + B[t, j] = best + + current = np.argmax(V[T - 1, :]) + sequence = [current] + for t in range(T - 1, 0, -1): + last = B[t, current] + sequence.append(last) + current = last + + sequence.reverse() + return list(map(self._states.__getitem__, sequence)) + + def best_path_simple(self, unlabeled_sequence): + """ + Returns the state sequence of the optimal (most probable) path through + the HMM. Uses the Viterbi algorithm to calculate this part by dynamic + programming. This uses a simple, direct method, and is included for + teaching purposes. + + :return: the state sequence + :rtype: sequence of any + :param unlabeled_sequence: the sequence of unlabeled symbols + :type unlabeled_sequence: list + """ + unlabeled_sequence = self._transform(unlabeled_sequence) + return self._best_path_simple(unlabeled_sequence) + + def _best_path_simple(self, unlabeled_sequence): + T = len(unlabeled_sequence) + N = len(self._states) + V = np.zeros((T, N), np.float64) + B = {} + + # find the starting log probabilities for each state + symbol = unlabeled_sequence[0] + for i, state in enumerate(self._states): + V[0, i] = self._priors.logprob(state) + self._output_logprob(state, symbol) + B[0, state] = None + + # find the maximum log probabilities for reaching each state at time t + for t in range(1, T): + symbol = unlabeled_sequence[t] + for j in range(N): + sj = self._states[j] + best = None + for i in range(N): + si = self._states[i] + va = V[t - 1, i] + self._transitions[si].logprob(sj) + if not best or va > best[0]: + best = (va, si) + V[t, j] = best[0] + self._output_logprob(sj, symbol) + B[t, sj] = best[1] + + # find the highest probability final state + best = None + for i in range(N): + val = V[T - 1, i] + if not best or val > best[0]: + best = (val, self._states[i]) + + # traverse the back-pointers B to find the state sequence + current = best[1] + sequence = [current] + for t in range(T - 1, 0, -1): + last = B[t, current] + sequence.append(last) + current = last + + sequence.reverse() + return sequence + + def random_sample(self, rng, length): + """ + Randomly sample the HMM to generate a sentence of a given length. This + samples the prior distribution then the observation distribution and + transition distribution for each subsequent observation and state. + This will mostly generate unintelligible garbage, but can provide some + amusement. + + :return: the randomly created state/observation sequence, + generated according to the HMM's probability + distributions. The SUBTOKENS have TEXT and TAG + properties containing the observation and state + respectively. + :rtype: list + :param rng: random number generator + :type rng: Random (or any object with a random() method) + :param length: desired output length + :type length: int + """ + + # sample the starting state and symbol prob dists + tokens = [] + state = self._sample_probdist(self._priors, rng.random(), self._states) + symbol = self._sample_probdist( + self._outputs[state], rng.random(), self._symbols + ) + tokens.append((symbol, state)) + + for i in range(1, length): + # sample the state transition and symbol prob dists + state = self._sample_probdist( + self._transitions[state], rng.random(), self._states + ) + symbol = self._sample_probdist( + self._outputs[state], rng.random(), self._symbols + ) + tokens.append((symbol, state)) + + return tokens + + def _sample_probdist(self, probdist, p, samples): + cum_p = 0 + for sample in samples: + add_p = probdist.prob(sample) + if cum_p <= p <= cum_p + add_p: + return sample + cum_p += add_p + raise Exception("Invalid probability distribution - " "does not sum to one") + + def entropy(self, unlabeled_sequence): + """ + Returns the entropy over labellings of the given sequence. This is + given by:: + + H(O) = - sum_S Pr(S | O) log Pr(S | O) + + where the summation ranges over all state sequences, S. Let + *Z = Pr(O) = sum_S Pr(S, O)}* where the summation ranges over all state + sequences and O is the observation sequence. As such the entropy can + be re-expressed as:: + + H = - sum_S Pr(S | O) log [ Pr(S, O) / Z ] + = log Z - sum_S Pr(S | O) log Pr(S, 0) + = log Z - sum_S Pr(S | O) [ log Pr(S_0) + sum_t Pr(S_t | S_{t-1}) + sum_t Pr(O_t | S_t) ] + + The order of summation for the log terms can be flipped, allowing + dynamic programming to be used to calculate the entropy. Specifically, + we use the forward and backward probabilities (alpha, beta) giving:: + + H = log Z - sum_s0 alpha_0(s0) beta_0(s0) / Z * log Pr(s0) + + sum_t,si,sj alpha_t(si) Pr(sj | si) Pr(O_t+1 | sj) beta_t(sj) / Z * log Pr(sj | si) + + sum_t,st alpha_t(st) beta_t(st) / Z * log Pr(O_t | st) + + This simply uses alpha and beta to find the probabilities of partial + sequences, constrained to include the given state(s) at some point in + time. + """ + unlabeled_sequence = self._transform(unlabeled_sequence) + + T = len(unlabeled_sequence) + N = len(self._states) + + alpha = self._forward_probability(unlabeled_sequence) + beta = self._backward_probability(unlabeled_sequence) + normalisation = logsumexp2(alpha[T - 1]) + + entropy = normalisation + + # starting state, t = 0 + for i, state in enumerate(self._states): + p = 2 ** (alpha[0, i] + beta[0, i] - normalisation) + entropy -= p * self._priors.logprob(state) + # print('p(s_0 = %s) =' % state, p) + + # state transitions + for t0 in range(T - 1): + t1 = t0 + 1 + for i0, s0 in enumerate(self._states): + for i1, s1 in enumerate(self._states): + p = 2 ** ( + alpha[t0, i0] + + self._transitions[s0].logprob(s1) + + self._outputs[s1].logprob(unlabeled_sequence[t1][_TEXT]) + + beta[t1, i1] + - normalisation + ) + entropy -= p * self._transitions[s0].logprob(s1) + # print('p(s_%d = %s, s_%d = %s) =' % (t0, s0, t1, s1), p) + + # symbol emissions + for t in range(T): + for i, state in enumerate(self._states): + p = 2 ** (alpha[t, i] + beta[t, i] - normalisation) + entropy -= p * self._outputs[state].logprob( + unlabeled_sequence[t][_TEXT] + ) + # print('p(s_%d = %s) =' % (t, state), p) + + return entropy + + def point_entropy(self, unlabeled_sequence): + """ + Returns the pointwise entropy over the possible states at each + position in the chain, given the observation sequence. + """ + unlabeled_sequence = self._transform(unlabeled_sequence) + + T = len(unlabeled_sequence) + N = len(self._states) + + alpha = self._forward_probability(unlabeled_sequence) + beta = self._backward_probability(unlabeled_sequence) + normalisation = logsumexp2(alpha[T - 1]) + + entropies = np.zeros(T, np.float64) + probs = np.zeros(N, np.float64) + for t in range(T): + for s in range(N): + probs[s] = alpha[t, s] + beta[t, s] - normalisation + + for s in range(N): + entropies[t] -= 2 ** (probs[s]) * probs[s] + + return entropies + + def _exhaustive_entropy(self, unlabeled_sequence): + unlabeled_sequence = self._transform(unlabeled_sequence) + + T = len(unlabeled_sequence) + N = len(self._states) + + labellings = [[state] for state in self._states] + for t in range(T - 1): + current = labellings + labellings = [] + for labelling in current: + for state in self._states: + labellings.append(labelling + [state]) + + log_probs = [] + for labelling in labellings: + labeled_sequence = unlabeled_sequence[:] + for t, label in enumerate(labelling): + labeled_sequence[t] = (labeled_sequence[t][_TEXT], label) + lp = self.log_probability(labeled_sequence) + log_probs.append(lp) + normalisation = _log_add(*log_probs) + + entropy = 0 + for lp in log_probs: + lp -= normalisation + entropy -= 2 ** (lp) * lp + + return entropy + + def _exhaustive_point_entropy(self, unlabeled_sequence): + unlabeled_sequence = self._transform(unlabeled_sequence) + + T = len(unlabeled_sequence) + N = len(self._states) + + labellings = [[state] for state in self._states] + for t in range(T - 1): + current = labellings + labellings = [] + for labelling in current: + for state in self._states: + labellings.append(labelling + [state]) + + log_probs = [] + for labelling in labellings: + labelled_sequence = unlabeled_sequence[:] + for t, label in enumerate(labelling): + labelled_sequence[t] = (labelled_sequence[t][_TEXT], label) + lp = self.log_probability(labelled_sequence) + log_probs.append(lp) + + normalisation = _log_add(*log_probs) + + probabilities = _ninf_array((T, N)) + + for labelling, lp in zip(labellings, log_probs): + lp -= normalisation + for t, label in enumerate(labelling): + index = self._states.index(label) + probabilities[t, index] = _log_add(probabilities[t, index], lp) + + entropies = np.zeros(T, np.float64) + for t in range(T): + for s in range(N): + entropies[t] -= 2 ** (probabilities[t, s]) * probabilities[t, s] + + return entropies + + def _transitions_matrix(self): + """Return a matrix of transition log probabilities.""" + trans_iter = ( + self._transitions[sj].logprob(si) + for sj in self._states + for si in self._states + ) + + transitions_logprob = np.fromiter(trans_iter, dtype=np.float64) + N = len(self._states) + return transitions_logprob.reshape((N, N)).T + + def _outputs_vector(self, symbol): + """ + Return a vector with log probabilities of emitting a symbol + when entering states. + """ + out_iter = (self._output_logprob(sj, symbol) for sj in self._states) + return np.fromiter(out_iter, dtype=np.float64) + + def _forward_probability(self, unlabeled_sequence): + """ + Return the forward probability matrix, a T by N array of + log-probabilities, where T is the length of the sequence and N is the + number of states. Each entry (t, s) gives the probability of being in + state s at time t after observing the partial symbol sequence up to + and including t. + + :param unlabeled_sequence: the sequence of unlabeled symbols + :type unlabeled_sequence: list + :return: the forward log probability matrix + :rtype: array + """ + T = len(unlabeled_sequence) + N = len(self._states) + alpha = _ninf_array((T, N)) + + transitions_logprob = self._transitions_matrix() + + # Initialization + symbol = unlabeled_sequence[0][_TEXT] + for i, state in enumerate(self._states): + alpha[0, i] = self._priors.logprob(state) + self._output_logprob( + state, symbol + ) + + # Induction + for t in range(1, T): + symbol = unlabeled_sequence[t][_TEXT] + output_logprob = self._outputs_vector(symbol) + + for i in range(N): + summand = alpha[t - 1] + transitions_logprob[i] + alpha[t, i] = logsumexp2(summand) + output_logprob[i] + + return alpha + + def _backward_probability(self, unlabeled_sequence): + """ + Return the backward probability matrix, a T by N array of + log-probabilities, where T is the length of the sequence and N is the + number of states. Each entry (t, s) gives the probability of being in + state s at time t after observing the partial symbol sequence from t + .. T. + + :return: the backward log probability matrix + :rtype: array + :param unlabeled_sequence: the sequence of unlabeled symbols + :type unlabeled_sequence: list + """ + T = len(unlabeled_sequence) + N = len(self._states) + beta = _ninf_array((T, N)) + + transitions_logprob = self._transitions_matrix().T + + # initialise the backward values; + # "1" is an arbitrarily chosen value from Rabiner tutorial + beta[T - 1, :] = np.log2(1) + + # inductively calculate remaining backward values + for t in range(T - 2, -1, -1): + symbol = unlabeled_sequence[t + 1][_TEXT] + outputs = self._outputs_vector(symbol) + + for i in range(N): + summand = transitions_logprob[i] + beta[t + 1] + outputs + beta[t, i] = logsumexp2(summand) + + return beta + + def test(self, test_sequence, verbose=False, **kwargs): + """ + Tests the HiddenMarkovModelTagger instance. + + :param test_sequence: a sequence of labeled test instances + :type test_sequence: list(list) + :param verbose: boolean flag indicating whether training should be + verbose or include printed output + :type verbose: bool + """ + + def words(sent): + return [word for (word, tag) in sent] + + def tags(sent): + return [tag for (word, tag) in sent] + + def flatten(seq): + return list(itertools.chain(*seq)) + + test_sequence = self._transform(test_sequence) + predicted_sequence = list(map(self._tag, map(words, test_sequence))) + + if verbose: + for test_sent, predicted_sent in zip(test_sequence, predicted_sequence): + print( + "Test:", + " ".join(f"{token}/{tag}" for (token, tag) in test_sent), + ) + print() + print("Untagged:", " ".join("%s" % token for (token, tag) in test_sent)) + print() + print( + "HMM-tagged:", + " ".join(f"{token}/{tag}" for (token, tag) in predicted_sent), + ) + print() + print( + "Entropy:", + self.entropy([(token, None) for (token, tag) in predicted_sent]), + ) + print() + print("-" * 60) + + test_tags = flatten(map(tags, test_sequence)) + predicted_tags = flatten(map(tags, predicted_sequence)) + + acc = accuracy(test_tags, predicted_tags) + count = sum(len(sent) for sent in test_sequence) + print("accuracy over %d tokens: %.2f" % (count, acc * 100)) + + def __repr__(self): + return "" % ( + len(self._states), + len(self._symbols), + ) + + +class HiddenMarkovModelTrainer: + """ + Algorithms for learning HMM parameters from training data. These include + both supervised learning (MLE) and unsupervised learning (Baum-Welch). + + Creates an HMM trainer to induce an HMM with the given states and + output symbol alphabet. A supervised and unsupervised training + method may be used. If either of the states or symbols are not given, + these may be derived from supervised training. + + :param states: the set of state labels + :type states: sequence of any + :param symbols: the set of observation symbols + :type symbols: sequence of any + """ + + def __init__(self, states=None, symbols=None): + self._states = states if states else [] + self._symbols = symbols if symbols else [] + + def train(self, labeled_sequences=None, unlabeled_sequences=None, **kwargs): + """ + Trains the HMM using both (or either of) supervised and unsupervised + techniques. + + :return: the trained model + :rtype: HiddenMarkovModelTagger + :param labelled_sequences: the supervised training data, a set of + labelled sequences of observations + ex: [ (word_1, tag_1),...,(word_n,tag_n) ] + :type labelled_sequences: list + :param unlabeled_sequences: the unsupervised training data, a set of + sequences of observations + ex: [ word_1, ..., word_n ] + :type unlabeled_sequences: list + :param kwargs: additional arguments to pass to the training methods + """ + assert labeled_sequences or unlabeled_sequences + model = None + if labeled_sequences: + model = self.train_supervised(labeled_sequences, **kwargs) + if unlabeled_sequences: + if model: + kwargs["model"] = model + model = self.train_unsupervised(unlabeled_sequences, **kwargs) + return model + + def _baum_welch_step(self, sequence, model, symbol_to_number): + N = len(model._states) + M = len(model._symbols) + T = len(sequence) + + # compute forward and backward probabilities + alpha = model._forward_probability(sequence) + beta = model._backward_probability(sequence) + + # find the log probability of the sequence + lpk = logsumexp2(alpha[T - 1]) + + A_numer = _ninf_array((N, N)) + B_numer = _ninf_array((N, M)) + A_denom = _ninf_array(N) + B_denom = _ninf_array(N) + + transitions_logprob = model._transitions_matrix().T + + for t in range(T): + symbol = sequence[t][_TEXT] # not found? FIXME + next_symbol = None + if t < T - 1: + next_symbol = sequence[t + 1][_TEXT] # not found? FIXME + xi = symbol_to_number[symbol] + + next_outputs_logprob = model._outputs_vector(next_symbol) + alpha_plus_beta = alpha[t] + beta[t] + + if t < T - 1: + numer_add = ( + transitions_logprob + + next_outputs_logprob + + beta[t + 1] + + alpha[t].reshape(N, 1) + ) + A_numer = np.logaddexp2(A_numer, numer_add) + A_denom = np.logaddexp2(A_denom, alpha_plus_beta) + else: + B_denom = np.logaddexp2(A_denom, alpha_plus_beta) + + B_numer[:, xi] = np.logaddexp2(B_numer[:, xi], alpha_plus_beta) + + return lpk, A_numer, A_denom, B_numer, B_denom + + def train_unsupervised(self, unlabeled_sequences, update_outputs=True, **kwargs): + """ + Trains the HMM using the Baum-Welch algorithm to maximise the + probability of the data sequence. This is a variant of the EM + algorithm, and is unsupervised in that it doesn't need the state + sequences for the symbols. The code is based on 'A Tutorial on Hidden + Markov Models and Selected Applications in Speech Recognition', + Lawrence Rabiner, IEEE, 1989. + + :return: the trained model + :rtype: HiddenMarkovModelTagger + :param unlabeled_sequences: the training data, a set of + sequences of observations + :type unlabeled_sequences: list + + kwargs may include following parameters: + + :param model: a HiddenMarkovModelTagger instance used to begin + the Baum-Welch algorithm + :param max_iterations: the maximum number of EM iterations + :param convergence_logprob: the maximum change in log probability to + allow convergence + """ + + # create a uniform HMM, which will be iteratively refined, unless + # given an existing model + model = kwargs.get("model") + if not model: + priors = RandomProbDist(self._states) + transitions = DictionaryConditionalProbDist( + {state: RandomProbDist(self._states) for state in self._states} + ) + outputs = DictionaryConditionalProbDist( + {state: RandomProbDist(self._symbols) for state in self._states} + ) + model = HiddenMarkovModelTagger( + self._symbols, self._states, transitions, outputs, priors + ) + + self._states = model._states + self._symbols = model._symbols + + N = len(self._states) + M = len(self._symbols) + symbol_numbers = {sym: i for i, sym in enumerate(self._symbols)} + + # update model prob dists so that they can be modified + # model._priors = MutableProbDist(model._priors, self._states) + + model._transitions = DictionaryConditionalProbDist( + { + s: MutableProbDist(model._transitions[s], self._states) + for s in self._states + } + ) + + if update_outputs: + model._outputs = DictionaryConditionalProbDist( + { + s: MutableProbDist(model._outputs[s], self._symbols) + for s in self._states + } + ) + + model.reset_cache() + + # iterate until convergence + converged = False + last_logprob = None + iteration = 0 + max_iterations = kwargs.get("max_iterations", 1000) + epsilon = kwargs.get("convergence_logprob", 1e-6) + + while not converged and iteration < max_iterations: + A_numer = _ninf_array((N, N)) + B_numer = _ninf_array((N, M)) + A_denom = _ninf_array(N) + B_denom = _ninf_array(N) + + logprob = 0 + for sequence in unlabeled_sequences: + sequence = list(sequence) + if not sequence: + continue + + ( + lpk, + seq_A_numer, + seq_A_denom, + seq_B_numer, + seq_B_denom, + ) = self._baum_welch_step(sequence, model, symbol_numbers) + + # add these sums to the global A and B values + for i in range(N): + A_numer[i] = np.logaddexp2(A_numer[i], seq_A_numer[i] - lpk) + B_numer[i] = np.logaddexp2(B_numer[i], seq_B_numer[i] - lpk) + + A_denom = np.logaddexp2(A_denom, seq_A_denom - lpk) + B_denom = np.logaddexp2(B_denom, seq_B_denom - lpk) + + logprob += lpk + + # use the calculated values to update the transition and output + # probability values + for i in range(N): + logprob_Ai = A_numer[i] - A_denom[i] + logprob_Bi = B_numer[i] - B_denom[i] + + # We should normalize all probabilities (see p.391 Huang et al) + # Let sum(P) be K. + # We can divide each Pi by K to make sum(P) == 1. + # Pi' = Pi/K + # log2(Pi') = log2(Pi) - log2(K) + logprob_Ai -= logsumexp2(logprob_Ai) + logprob_Bi -= logsumexp2(logprob_Bi) + + # update output and transition probabilities + si = self._states[i] + + for j in range(N): + sj = self._states[j] + model._transitions[si].update(sj, logprob_Ai[j]) + + if update_outputs: + for k in range(M): + ok = self._symbols[k] + model._outputs[si].update(ok, logprob_Bi[k]) + + # Rabiner says the priors don't need to be updated. I don't + # believe him. FIXME + + # test for convergence + if iteration > 0 and abs(logprob - last_logprob) < epsilon: + converged = True + + print("iteration", iteration, "logprob", logprob) + iteration += 1 + last_logprob = logprob + + return model + + def train_supervised(self, labelled_sequences, estimator=None): + """ + Supervised training maximising the joint probability of the symbol and + state sequences. This is done via collecting frequencies of + transitions between states, symbol observations while within each + state and which states start a sentence. These frequency distributions + are then normalised into probability estimates, which can be + smoothed if desired. + + :return: the trained model + :rtype: HiddenMarkovModelTagger + :param labelled_sequences: the training data, a set of + labelled sequences of observations + :type labelled_sequences: list + :param estimator: a function taking + a FreqDist and a number of bins and returning a CProbDistI; + otherwise a MLE estimate is used + """ + + # default to the MLE estimate + if estimator is None: + estimator = lambda fdist, bins: MLEProbDist(fdist) + + # count occurrences of starting states, transitions out of each state + # and output symbols observed in each state + known_symbols = set(self._symbols) + known_states = set(self._states) + + starting = FreqDist() + transitions = ConditionalFreqDist() + outputs = ConditionalFreqDist() + for sequence in labelled_sequences: + lasts = None + for token in sequence: + state = token[_TAG] + symbol = token[_TEXT] + if lasts is None: + starting[state] += 1 + else: + transitions[lasts][state] += 1 + outputs[state][symbol] += 1 + lasts = state + + # update the state and symbol lists + if state not in known_states: + self._states.append(state) + known_states.add(state) + + if symbol not in known_symbols: + self._symbols.append(symbol) + known_symbols.add(symbol) + + # create probability distributions (with smoothing) + N = len(self._states) + pi = estimator(starting, N) + A = ConditionalProbDist(transitions, estimator, N) + B = ConditionalProbDist(outputs, estimator, len(self._symbols)) + + return HiddenMarkovModelTagger(self._symbols, self._states, A, B, pi) + + +def _ninf_array(shape): + res = np.empty(shape, np.float64) + res.fill(-np.inf) + return res + + +def logsumexp2(arr): + max_ = arr.max() + return np.log2(np.sum(2 ** (arr - max_))) + max_ + + +def _log_add(*values): + """ + Adds the logged values, returning the logarithm of the addition. + """ + x = max(values) + if x > -np.inf: + sum_diffs = 0 + for value in values: + sum_diffs += 2 ** (value - x) + return x + np.log2(sum_diffs) + else: + return x + + +def _create_hmm_tagger(states, symbols, A, B, pi): + def pd(values, samples): + d = dict(zip(samples, values)) + return DictionaryProbDist(d) + + def cpd(array, conditions, samples): + d = {} + for values, condition in zip(array, conditions): + d[condition] = pd(values, samples) + return DictionaryConditionalProbDist(d) + + A = cpd(A, states, states) + B = cpd(B, states, symbols) + pi = pd(pi, states) + return HiddenMarkovModelTagger( + symbols=symbols, states=states, transitions=A, outputs=B, priors=pi + ) + + +def _market_hmm_example(): + """ + Return an example HMM (described at page 381, Huang et al) + """ + states = ["bull", "bear", "static"] + symbols = ["up", "down", "unchanged"] + A = np.array([[0.6, 0.2, 0.2], [0.5, 0.3, 0.2], [0.4, 0.1, 0.5]], np.float64) + B = np.array([[0.7, 0.1, 0.2], [0.1, 0.6, 0.3], [0.3, 0.3, 0.4]], np.float64) + pi = np.array([0.5, 0.2, 0.3], np.float64) + + model = _create_hmm_tagger(states, symbols, A, B, pi) + return model, states, symbols + + +def demo(): + # demonstrates HMM probability calculation + + print() + print("HMM probability calculation demo") + print() + + model, states, symbols = _market_hmm_example() + + print("Testing", model) + + for test in [ + ["up", "up"], + ["up", "down", "up"], + ["down"] * 5, + ["unchanged"] * 5 + ["up"], + ]: + sequence = [(t, None) for t in test] + + print("Testing with state sequence", test) + print("probability =", model.probability(sequence)) + print("tagging = ", model.tag([word for (word, tag) in sequence])) + print("p(tagged) = ", model.probability(sequence)) + print("H = ", model.entropy(sequence)) + print("H_exh = ", model._exhaustive_entropy(sequence)) + print("H(point) = ", model.point_entropy(sequence)) + print("H_exh(point)=", model._exhaustive_point_entropy(sequence)) + print() + + +def load_pos(num_sents): + from nltk.corpus import brown + + sentences = brown.tagged_sents(categories="news")[:num_sents] + + tag_re = re.compile(r"[*]|--|[^+*-]+") + tag_set = set() + symbols = set() + + cleaned_sentences = [] + for sentence in sentences: + for i in range(len(sentence)): + word, tag = sentence[i] + word = word.lower() # normalize + symbols.add(word) # log this word + # Clean up the tag. + tag = tag_re.match(tag).group() + tag_set.add(tag) + sentence[i] = (word, tag) # store cleaned-up tagged token + cleaned_sentences += [sentence] + + return cleaned_sentences, list(tag_set), list(symbols) + + +def demo_pos(): + # demonstrates POS tagging using supervised training + + print() + print("HMM POS tagging demo") + print() + + print("Training HMM...") + labelled_sequences, tag_set, symbols = load_pos(20000) + trainer = HiddenMarkovModelTrainer(tag_set, symbols) + hmm = trainer.train_supervised( + labelled_sequences[10:], + estimator=lambda fd, bins: LidstoneProbDist(fd, 0.1, bins), + ) + + print("Testing...") + hmm.test(labelled_sequences[:10], verbose=True) + + +def _untag(sentences): + unlabeled = [] + for sentence in sentences: + unlabeled.append([(token[_TEXT], None) for token in sentence]) + return unlabeled + + +def demo_pos_bw( + test=10, supervised=20, unsupervised=10, verbose=True, max_iterations=5 +): + # demonstrates the Baum-Welch algorithm in POS tagging + + print() + print("Baum-Welch demo for POS tagging") + print() + + print("Training HMM (supervised, %d sentences)..." % supervised) + + sentences, tag_set, symbols = load_pos(test + supervised + unsupervised) + + symbols = set() + for sentence in sentences: + for token in sentence: + symbols.add(token[_TEXT]) + + trainer = HiddenMarkovModelTrainer(tag_set, list(symbols)) + hmm = trainer.train_supervised( + sentences[test : test + supervised], + estimator=lambda fd, bins: LidstoneProbDist(fd, 0.1, bins), + ) + + hmm.test(sentences[:test], verbose=verbose) + + print("Training (unsupervised, %d sentences)..." % unsupervised) + # it's rather slow - so only use 10 samples by default + unlabeled = _untag(sentences[test + supervised :]) + hmm = trainer.train_unsupervised( + unlabeled, model=hmm, max_iterations=max_iterations + ) + hmm.test(sentences[:test], verbose=verbose) + + +def demo_bw(): + # demo Baum Welch by generating some sequences and then performing + # unsupervised training on them + + print() + print("Baum-Welch demo for market example") + print() + + model, states, symbols = _market_hmm_example() + + # generate some random sequences + training = [] + import random + + rng = random.Random() + rng.seed(0) + for i in range(10): + item = model.random_sample(rng, 5) + training.append([(i[0], None) for i in item]) + + # train on those examples, starting with the model that generated them + trainer = HiddenMarkovModelTrainer(states, symbols) + hmm = trainer.train_unsupervised(training, model=model, max_iterations=1000) diff --git a/nltk/tag/hunpos.py b/nltk/tag/hunpos.py new file mode 100644 index 0000000..4ae671f --- /dev/null +++ b/nltk/tag/hunpos.py @@ -0,0 +1,142 @@ +# Natural Language Toolkit: Interface to the HunPos POS-tagger +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Peter Ljunglöf +# Dávid Márk Nemeskey (modifications) +# Attila Zséder (modifications) +# URL: +# For license information, see LICENSE.TXT + +""" +A module for interfacing with the HunPos open-source POS-tagger. +""" + +import os +from subprocess import PIPE, Popen + +from nltk.internals import find_binary, find_file +from nltk.tag.api import TaggerI + +_hunpos_url = "https://code.google.com/p/hunpos/" + +_hunpos_charset = "ISO-8859-1" +"""The default encoding used by hunpos: ISO-8859-1.""" + + +class HunposTagger(TaggerI): + """ + A class for pos tagging with HunPos. The input is the paths to: + - a model trained on training data + - (optionally) the path to the hunpos-tag binary + - (optionally) the encoding of the training data (default: ISO-8859-1) + + Check whether the required "hunpos-tag" binary is available: + + >>> from nltk.test.setup_fixt import check_binary + >>> check_binary('hunpos-tag') + + Example: + >>> from nltk.tag import HunposTagger + >>> ht = HunposTagger('en_wsj.model') + >>> ht.tag('What is the airspeed of an unladen swallow ?'.split()) + [('What', 'WP'), ('is', 'VBZ'), ('the', 'DT'), ('airspeed', 'NN'), ('of', 'IN'), ('an', 'DT'), ('unladen', 'NN'), ('swallow', 'VB'), ('?', '.')] + >>> ht.close() + + This class communicates with the hunpos-tag binary via pipes. When the + tagger object is no longer needed, the close() method should be called to + free system resources. The class supports the context manager interface; if + used in a with statement, the close() method is invoked automatically: + + >>> with HunposTagger('en_wsj.model') as ht: + ... ht.tag('What is the airspeed of an unladen swallow ?'.split()) + ... + [('What', 'WP'), ('is', 'VBZ'), ('the', 'DT'), ('airspeed', 'NN'), ('of', 'IN'), ('an', 'DT'), ('unladen', 'NN'), ('swallow', 'VB'), ('?', '.')] + """ + + def __init__( + self, path_to_model, path_to_bin=None, encoding=_hunpos_charset, verbose=False + ): + """ + Starts the hunpos-tag executable and establishes a connection with it. + + :param path_to_model: The model file. + :param path_to_bin: The hunpos-tag binary. + :param encoding: The encoding used by the model. Unicode tokens + passed to the tag() and tag_sents() methods are converted to + this charset when they are sent to hunpos-tag. + The default is ISO-8859-1 (Latin-1). + + This parameter is ignored for str tokens, which are sent as-is. + The caller must ensure that tokens are encoded in the right charset. + """ + self._closed = True + hunpos_paths = [ + ".", + "/usr/bin", + "/usr/local/bin", + "/opt/local/bin", + "/Applications/bin", + "~/bin", + "~/Applications/bin", + ] + hunpos_paths = list(map(os.path.expanduser, hunpos_paths)) + + self._hunpos_bin = find_binary( + "hunpos-tag", + path_to_bin, + env_vars=("HUNPOS_TAGGER",), + searchpath=hunpos_paths, + url=_hunpos_url, + verbose=verbose, + ) + + self._hunpos_model = find_file( + path_to_model, env_vars=("HUNPOS_TAGGER",), verbose=verbose + ) + self._encoding = encoding + self._hunpos = Popen( + [self._hunpos_bin, self._hunpos_model], + shell=False, + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + ) + self._closed = False + + def __del__(self): + self.close() + + def close(self): + """Closes the pipe to the hunpos executable.""" + if not self._closed: + self._hunpos.communicate() + self._closed = True + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def tag(self, tokens): + """Tags a single sentence: a list of words. + The tokens should not contain any newline characters. + """ + for token in tokens: + assert "\n" not in token, "Tokens should not contain newlines" + if isinstance(token, str): + token = token.encode(self._encoding) + self._hunpos.stdin.write(token + b"\n") + # We write a final empty line to tell hunpos that the sentence is finished: + self._hunpos.stdin.write(b"\n") + self._hunpos.stdin.flush() + + tagged_tokens = [] + for token in tokens: + tagged = self._hunpos.stdout.readline().strip().split(b"\t") + tag = tagged[1] if len(tagged) > 1 else None + tagged_tokens.append((token, tag)) + # We have to read (and dismiss) the final empty line: + self._hunpos.stdout.readline() + + return tagged_tokens diff --git a/nltk/tag/mapping.py b/nltk/tag/mapping.py new file mode 100644 index 0000000..5971f98 --- /dev/null +++ b/nltk/tag/mapping.py @@ -0,0 +1,145 @@ +# Natural Language Toolkit: Tagset Mapping +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Nathan Schneider +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Interface for converting POS tags from various treebanks +to the universal tagset of Petrov, Das, & McDonald. + +The tagset consists of the following 12 coarse tags: + +VERB - verbs (all tenses and modes) +NOUN - nouns (common and proper) +PRON - pronouns +ADJ - adjectives +ADV - adverbs +ADP - adpositions (prepositions and postpositions) +CONJ - conjunctions +DET - determiners +NUM - cardinal numbers +PRT - particles or other function words +X - other: foreign words, typos, abbreviations +. - punctuation + +@see: https://arxiv.org/abs/1104.2086 and https://code.google.com/p/universal-pos-tags/ + +""" + +from collections import defaultdict +from os.path import join + +from nltk.data import load, normalize_resource_url + +_UNIVERSAL_DATA = "taggers/universal_tagset/" +_UNIVERSAL_TAGS = ( + "VERB", + "NOUN", + "PRON", + "ADJ", + "ADV", + "ADP", + "CONJ", + "DET", + "NUM", + "PRT", + "X", + ".", +) + +# _MAPPINGS = defaultdict(lambda: defaultdict(dict)) +# the mapping between tagset T1 and T2 returns UNK if applied to an unrecognized tag +_MAPPINGS = defaultdict(lambda: defaultdict(lambda: defaultdict(lambda: "UNK"))) + + +def _load_universal_map(fileid): + resource = normalize_resource_url( + f"nltk:{_UNIVERSAL_DATA.rstrip('/')}/{fileid.lstrip('/')}.map" + ) + contents = load(resource, format="text") + + # When mapping to the Universal Tagset, + # map unknown inputs to 'X' not 'UNK' + _MAPPINGS[fileid]["universal"].default_factory = lambda: "X" + + for line in contents.splitlines(): + line = line.strip() + if line == "": + continue + fine, coarse = line.split("\t") + + assert coarse in _UNIVERSAL_TAGS, f"Unexpected coarse tag: {coarse}" + assert ( + fine not in _MAPPINGS[fileid]["universal"] + ), f"Multiple entries for original tag: {fine}" + + _MAPPINGS[fileid]["universal"][fine] = coarse + + +def tagset_mapping(source, target): + """ + Retrieve the mapping dictionary between tagsets. + + >>> tagset_mapping('ru-rnc', 'universal') == {'!': '.', 'A': 'ADJ', 'C': 'CONJ', 'AD': 'ADV',\ + 'NN': 'NOUN', 'VG': 'VERB', 'COMP': 'CONJ', 'NC': 'NUM', 'VP': 'VERB', 'P': 'ADP',\ + 'IJ': 'X', 'V': 'VERB', 'Z': 'X', 'VI': 'VERB', 'YES_NO_SENT': 'X', 'PTCL': 'PRT'} + True + """ + + if source not in _MAPPINGS or target not in _MAPPINGS[source]: + if target == "universal": + if source == "ru-rnc-new": + # ru-rnc-new.map is not shipped in the universal_tagset data package, + # so avoid _load_universal_map("ru-rnc-new") and use the embedded mapping. + # Ensure unknown tags map to 'X' (same behavior as _load_universal_map()). + _MAPPINGS[source][target].default_factory = lambda: "X" + _MAPPINGS[source][target].update( + { + "A": "ADJ", + "A-PRO": "PRON", + "ADV": "ADV", + "ADV-PRO": "PRON", + "ANUM": "ADJ", + "CONJ": "CONJ", + "INTJ": "X", + "NONLEX": ".", + "NUM": "NUM", + "PARENTH": "PRT", + "PART": "PRT", + "PR": "ADP", + "PRAEDIC": "PRT", + "PRAEDIC-PRO": "PRON", + "S": "NOUN", + "S-PRO": "PRON", + "V": "VERB", + } + ) + else: + _load_universal_map(source) + + return _MAPPINGS[source][target] + + +def map_tag(source, target, source_tag): + """ + Maps the tag from the source tagset to the target tagset. + + >>> map_tag('en-ptb', 'universal', 'VBZ') + 'VERB' + >>> map_tag('en-ptb', 'universal', 'VBP') + 'VERB' + >>> map_tag('en-ptb', 'universal', '``') + '.' + """ + + # we need a systematic approach to naming + if target == "universal": + if source == "wsj": + source = "en-ptb" + if source == "brown": + source = "en-brown" + + return tagset_mapping(source, target)[source_tag] diff --git a/nltk/tag/perceptron.py b/nltk/tag/perceptron.py new file mode 100644 index 0000000..024cd60 --- /dev/null +++ b/nltk/tag/perceptron.py @@ -0,0 +1,407 @@ +# This module is a port of the Textblob Averaged Perceptron Tagger +# Author: Matthew Honnibal , +# Long Duong (NLTK port) +# URL: +# +# Copyright 2013 Matthew Honnibal +# NLTK modifications Copyright 2015 The NLTK Project +# +# This module is provided under the terms of the MIT License. + +import json +import logging +import os +import random +from collections import defaultdict +from os.path import join as path_join +from pathlib import Path +from tempfile import gettempdir + +from nltk import jsontags +from nltk.data import FileSystemPathPointer, find, open_datafile +from nltk.tag.api import TaggerI + +try: + import numpy as np +except ImportError: + pass + + +@jsontags.register_tag +class AveragedPerceptron: + """An averaged perceptron, as implemented by Matthew Honnibal. + + See more implementation details here: + https://explosion.ai/blog/part-of-speech-pos-tagger-in-python + """ + + json_tag = "nltk.tag.perceptron.AveragedPerceptron" + + def __init__(self, weights=None): + # Each feature gets its own weight vector, so weights is a dict-of-dicts + self.weights = weights if weights else {} + self.classes = set() + # The accumulated values, for the averaging. These will be keyed by + # feature/clas tuples + self._totals = defaultdict(int) + # The last time the feature was changed, for the averaging. Also + # keyed by feature/clas tuples + # (tstamps is short for timestamps) + self._tstamps = defaultdict(int) + # Number of instances seen + self.i = 0 + + def _softmax(self, scores): + s = np.fromiter(scores.values(), dtype=float) + exps = np.exp(s) + return exps / np.sum(exps) + + def predict(self, features, return_conf=False): + """Dot-product the features and current weights and return the best label.""" + scores = defaultdict(float) + for feat, value in features.items(): + if feat not in self.weights or value == 0: + continue + weights = self.weights[feat] + for label, weight in weights.items(): + scores[label] += value * weight + + # Do a secondary alphabetic sort, for stability + best_label = max(self.classes, key=lambda label: (scores[label], label)) + # compute the confidence + conf = max(self._softmax(scores)) if return_conf else None + + return best_label, conf + + def update(self, truth, guess, features): + """Update the feature weights.""" + + def upd_feat(c, f, w, v): + param = (f, c) + self._totals[param] += (self.i - self._tstamps[param]) * w + self._tstamps[param] = self.i + self.weights[f][c] = w + v + + self.i += 1 + if truth == guess: + return None + for f in features: + weights = self.weights.setdefault(f, {}) + upd_feat(truth, f, weights.get(truth, 0.0), 1.0) + upd_feat(guess, f, weights.get(guess, 0.0), -1.0) + + def average_weights(self): + """Average weights from all iterations.""" + for feat, weights in self.weights.items(): + new_feat_weights = {} + for clas, weight in weights.items(): + param = (feat, clas) + total = self._totals[param] + total += (self.i - self._tstamps[param]) * weight + averaged = round(total / self.i, 3) + if averaged: + new_feat_weights[clas] = averaged + self.weights[feat] = new_feat_weights + + def save(self, path): + """Save the model weights as json""" + with open(path, "w") as fout: + return json.dump(self.weights, fout) + + def load(self, path): + """Load the json model weights.""" + with open(path) as fin: + self.weights = json.load(fin) + + def encode_json_obj(self): + return self.weights + + @classmethod + def decode_json_obj(cls, obj): + return cls(obj) + + +@jsontags.register_tag +class PerceptronTagger(TaggerI): + """ + Greedy Averaged Perceptron tagger, as implemented by Matthew Honnibal. + See more implementation details here: + https://explosion.ai/blog/part-of-speech-pos-tagger-in-python + + >>> from nltk.tag.perceptron import PerceptronTagger + >>> tagger = PerceptronTagger(load=False) + + Train and save the model: + + >>> tagger.train([[('today','NN'),('is','VBZ'),('good','JJ'),('day','NN')], + ... [('yes','NNS'),('it','PRP'),('beautiful','JJ')]], save_loc=tagger.save_dir) + + Load the saved model: + + >>> from nltk.data import FileSystemPathPointer + >>> tagger2 = PerceptronTagger(loc=FileSystemPathPointer(tagger.save_dir)) + >>> print(sorted(list(tagger2.classes))) + ['JJ', 'NN', 'NNS', 'PRP', 'VBZ'] + + >>> print(tagger2.classes == tagger.classes) + True + + >>> tagger2.tag(['today','is','a','beautiful','day']) + [('today', 'NN'), ('is', 'PRP'), ('a', 'PRP'), ('beautiful', 'JJ'), ('day', 'NN')] + + Use the pretrain model (the default constructor) + + >>> pretrain = PerceptronTagger() + + >>> pretrain.tag('The quick brown fox jumps over the lazy dog'.split()) + [('The', 'DT'), ('quick', 'JJ'), ('brown', 'NN'), ('fox', 'NN'), ('jumps', 'VBZ'), ('over', 'IN'), ('the', 'DT'), ('lazy', 'JJ'), ('dog', 'NN')] + + >>> pretrain.tag("The red cat".split()) + [('The', 'DT'), ('red', 'JJ'), ('cat', 'NN')] + """ + + json_tag = "nltk.tag.perceptron.PerceptronTagger" + + START = ["-START-", "-START2-"] + END = ["-END-", "-END2-"] + + def __init__(self, load=True, lang="eng", loc=None): + """ + :param load: Load the json model upon instantiation. + """ + self.model = AveragedPerceptron() + self.tagdict = {} + self.classes = set() + self.lang = lang + # Save trained models in tmp directory by default: + self.TRAINED_TAGGER_PATH = gettempdir() + self.TAGGER_NAME = "averaged_perceptron_tagger" + self.save_dir = path_join( + self.TRAINED_TAGGER_PATH, f"{self.TAGGER_NAME}_{self.lang}" + ) + if load: + self.load_from_json(lang, loc) + + def param_files(self, lang="eng"): + return ( + f"{self.TAGGER_NAME}_{lang}.{attr}.json" + for attr in ["weights", "tagdict", "classes"] + ) + + def tag(self, tokens, return_conf=False, use_tagdict=True): + """ + Tag tokenized sentences. + :params tokens: list of word + :type tokens: list(str) + """ + prev, prev2 = self.START + output = [] + + context = self.START + [self.normalize(w) for w in tokens] + self.END + for i, word in enumerate(tokens): + tag, conf = (self.tagdict.get(word), 1.0) if use_tagdict else (None, None) + if not tag: + features = self._get_features(i, word, context, prev, prev2) + tag, conf = self.model.predict(features, return_conf) + output.append((word, tag, conf) if return_conf else (word, tag)) + + prev2 = prev + prev = tag + + return output + + def train(self, sentences, save_loc=None, nr_iter=5): + """Train a model from sentences, and save it at ``save_loc``. ``nr_iter`` + controls the number of Perceptron training iterations. + + :param sentences: A list or iterator of sentences, where each sentence + is a list of (words, tags) tuples. + :param save_loc: If not ``None``, saves a json model in this location. + :param nr_iter: Number of training iterations. + """ + # We'd like to allow ``sentences`` to be either a list or an iterator, + # the latter being especially important for a large training dataset. + # Because ``self._make_tagdict(sentences)`` runs regardless, we make + # it populate ``self._sentences`` (a list) with all the sentences. + # This saves the overheard of just iterating through ``sentences`` to + # get the list by ``sentences = list(sentences)``. + + self._sentences = list() # to be populated by self._make_tagdict... + self._make_tagdict(sentences) + self.model.classes = self.classes + for iter_ in range(nr_iter): + c = 0 + n = 0 + for sentence in self._sentences: + words, tags = zip(*sentence) + + prev, prev2 = self.START + context = self.START + [self.normalize(w) for w in words] + self.END + for i, word in enumerate(words): + guess = self.tagdict.get(word) + if not guess: + feats = self._get_features(i, word, context, prev, prev2) + guess, _ = self.model.predict(feats) + self.model.update(tags[i], guess, feats) + prev2 = prev + prev = guess + c += guess == tags[i] + n += 1 + random.shuffle(self._sentences) + logging.info(f"Iter {iter_}: {c}/{n}={_pc(c, n)}") + + # We don't need the training sentences anymore, and we don't want to + # waste space on them when we the trained tagger. + self._sentences = None + + self.model.average_weights() + # Save to json files. + if save_loc is not None: + self.save_to_json(lang=self.lang, loc=save_loc) + + def save_to_json(self, lang="xxx", loc=None): + from os import mkdir + from os.path import isdir + + if not loc: + loc = self.save_dir + if not isdir(loc): + mkdir(loc) + + for param, json_file in zip(self.encode_json_obj(), self.param_files(lang)): + with open(path_join(loc, json_file), "w") as fout: + json.dump(param, fout) + + def load_from_json(self, lang="eng", loc=None): + # Automatically find path to the tagger if location is not specified. + # loc can refer to zip or real FS + if loc is None: + loc = find(f"taggers/averaged_perceptron_tagger_{lang}/") + elif isinstance(loc, str): + # Backward compatible: + # - absolute paths are explicit filesystem locations + # - relative strings are treated as NLTK resource names and resolved via find() + if os.path.isabs(loc): + loc = FileSystemPathPointer(loc) + else: + loc = find(loc) + elif isinstance(loc, Path): + # Explicit filesystem path + loc = FileSystemPathPointer(str(loc)) + # else: assume loc is already a PathPointer (zip or filesystem) + + def load_param(json_file): + with open_datafile(loc, json_file) as fin: + return json.load(fin) + + self.decode_json_params( + load_param(js_file) for js_file in self.param_files(lang) + ) + + def decode_json_params(self, params): + weights, tagdict, class_list = params + self.model.weights = weights + self.tagdict = tagdict + self.classes = self.model.classes = set(class_list) + + def encode_json_obj(self): + return self.model.weights, self.tagdict, list(self.classes) + + @classmethod + def decode_json_obj(cls, obj): + tagger = cls(load=False) + tagger.decode_json_params(obj) + return tagger + + def normalize(self, word): + """ + Normalization used in pre-processing. + - All words are lower cased + - Groups of digits of length 4 are represented as !YEAR; + - Other digits are represented as !DIGITS + + :rtype: str + """ + if "-" in word and word[0] != "-": + return "!HYPHEN" + if word.isdigit() and len(word) == 4: + return "!YEAR" + if word and word[0].isdigit(): + return "!DIGITS" + return word.lower() + + def _get_features(self, i, word, context, prev, prev2): + """Map tokens into a feature representation, implemented as a + {hashable: int} dict. If the features change, a new model must be + trained. + """ + + def add(name, *args): + features[" ".join((name,) + tuple(args))] += 1 + + i += len(self.START) + features = defaultdict(int) + # It's useful to have a constant feature, which acts sort of like a prior + add("bias") + add("i suffix", word[-3:]) + add("i pref1", word[0] if word else "") + add("i-1 tag", prev) + add("i-2 tag", prev2) + add("i tag+i-2 tag", prev, prev2) + add("i word", context[i]) + add("i-1 tag+i word", prev, context[i]) + add("i-1 word", context[i - 1]) + add("i-1 suffix", context[i - 1][-3:]) + add("i-2 word", context[i - 2]) + add("i+1 word", context[i + 1]) + add("i+1 suffix", context[i + 1][-3:]) + add("i+2 word", context[i + 2]) + return features + + def _make_tagdict(self, sentences): + """ + Make a tag dictionary for single-tag words. + :param sentences: A list of list of (word, tag) tuples. + """ + counts = defaultdict(lambda: defaultdict(int)) + for sentence in sentences: + self._sentences.append(sentence) + for word, tag in sentence: + counts[word][tag] += 1 + self.classes.add(tag) + freq_thresh = 20 + ambiguity_thresh = 0.97 + for word, tag_freqs in counts.items(): + tag, mode = max(tag_freqs.items(), key=lambda item: item[1]) + n = sum(tag_freqs.values()) + # Don't add rare words to the tag dictionary + # Only add quite unambiguous words + if n >= freq_thresh and (mode / n) >= ambiguity_thresh: + self.tagdict[word] = tag + + +def _pc(n, d): + return (n / d) * 100 + + +def _train_and_test(lang="sv"): + """ + Train and test on 'lang' part of universal_treebanks corpus, which includes + train and test sets in conll format for 'de', 'es', 'fi', 'fr' and 'sv'. + Finds 0.94 accuracy on 'sv' (Swedish) test set. + """ + from nltk.corpus import universal_treebanks as utb + + tagger = PerceptronTagger(load=False, lang=lang) + training = utb.tagged_sents(f"ch/{lang}/{lang}-universal-ch-train.conll") + testing = utb.tagged_sents(f"ch/{lang}/{lang}-universal-ch-test.conll") + print( + f"(Lang = {lang}) training on {len(training)} and testing on {len(testing)} sentences" + ) + # Train and save the model + tagger.train(training, save_loc=tagger.save_dir) + print("Accuracy : ", tagger.accuracy(testing)) + + +if __name__ == "__main__": + _train_and_test() diff --git a/nltk/tag/senna.py b/nltk/tag/senna.py new file mode 100644 index 0000000..9a2edc5 --- /dev/null +++ b/nltk/tag/senna.py @@ -0,0 +1,134 @@ +# Natural Language Toolkit: Senna POS Tagger +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Rami Al-Rfou' +# URL: +# For license information, see LICENSE.TXT + +""" +Senna POS tagger, NER Tagger, Chunk Tagger + +The input is: + +- path to the directory that contains SENNA executables. If the path is incorrect, + SennaTagger will automatically search for executable file specified in SENNA environment variable +- (optionally) the encoding of the input data (default:utf-8) + +Note: Unit tests for this module can be found in test/unit/test_senna.py + +>>> from nltk.tag import SennaTagger +>>> tagger = SennaTagger('/usr/share/senna-v3.0') # doctest: +SKIP +>>> tagger.tag('What is the airspeed of an unladen swallow ?'.split()) # doctest: +SKIP +[('What', 'WP'), ('is', 'VBZ'), ('the', 'DT'), ('airspeed', 'NN'), +('of', 'IN'), ('an', 'DT'), ('unladen', 'NN'), ('swallow', 'NN'), ('?', '.')] + +>>> from nltk.tag import SennaChunkTagger +>>> chktagger = SennaChunkTagger('/usr/share/senna-v3.0') # doctest: +SKIP +>>> chktagger.tag('What is the airspeed of an unladen swallow ?'.split()) # doctest: +SKIP +[('What', 'B-NP'), ('is', 'B-VP'), ('the', 'B-NP'), ('airspeed', 'I-NP'), +('of', 'B-PP'), ('an', 'B-NP'), ('unladen', 'I-NP'), ('swallow', 'I-NP'), +('?', 'O')] + +>>> from nltk.tag import SennaNERTagger +>>> nertagger = SennaNERTagger('/usr/share/senna-v3.0') # doctest: +SKIP +>>> nertagger.tag('Shakespeare theatre was in London .'.split()) # doctest: +SKIP +[('Shakespeare', 'B-PER'), ('theatre', 'O'), ('was', 'O'), ('in', 'O'), +('London', 'B-LOC'), ('.', 'O')] +>>> nertagger.tag('UN headquarters are in NY , USA .'.split()) # doctest: +SKIP +[('UN', 'B-ORG'), ('headquarters', 'O'), ('are', 'O'), ('in', 'O'), +('NY', 'B-LOC'), (',', 'O'), ('USA', 'B-LOC'), ('.', 'O')] +""" + +from nltk.classify import Senna + + +class SennaTagger(Senna): + def __init__(self, path, encoding="utf-8"): + super().__init__(path, ["pos"], encoding) + + def tag_sents(self, sentences): + """ + Applies the tag method over a list of sentences. This method will return + for each sentence a list of tuples of (word, tag). + """ + tagged_sents = super().tag_sents(sentences) + for i in range(len(tagged_sents)): + for j in range(len(tagged_sents[i])): + annotations = tagged_sents[i][j] + tagged_sents[i][j] = (annotations["word"], annotations["pos"]) + return tagged_sents + + +class SennaChunkTagger(Senna): + def __init__(self, path, encoding="utf-8"): + super().__init__(path, ["chk"], encoding) + + def tag_sents(self, sentences): + """ + Applies the tag method over a list of sentences. This method will return + for each sentence a list of tuples of (word, tag). + """ + tagged_sents = super().tag_sents(sentences) + for i in range(len(tagged_sents)): + for j in range(len(tagged_sents[i])): + annotations = tagged_sents[i][j] + tagged_sents[i][j] = (annotations["word"], annotations["chk"]) + return tagged_sents + + def bio_to_chunks(self, tagged_sent, chunk_type): + """ + Extracts the chunks in a BIO chunk-tagged sentence. + + >>> from nltk.tag import SennaChunkTagger + >>> chktagger = SennaChunkTagger('/usr/share/senna-v3.0') # doctest: +SKIP + >>> sent = 'What is the airspeed of an unladen swallow ?'.split() + >>> tagged_sent = chktagger.tag(sent) # doctest: +SKIP + >>> tagged_sent # doctest: +SKIP + [('What', 'B-NP'), ('is', 'B-VP'), ('the', 'B-NP'), ('airspeed', 'I-NP'), + ('of', 'B-PP'), ('an', 'B-NP'), ('unladen', 'I-NP'), ('swallow', 'I-NP'), + ('?', 'O')] + >>> list(chktagger.bio_to_chunks(tagged_sent, chunk_type='NP')) # doctest: +SKIP + [('What', '0'), ('the airspeed', '2-3'), ('an unladen swallow', '5-6-7')] + + :param tagged_sent: A list of tuples of word and BIO chunk tag. + :type tagged_sent: list(tuple) + :param tagged_sent: The chunk tag that users want to extract, e.g. 'NP' or 'VP' + :type tagged_sent: str + + :return: An iterable of tuples of chunks that users want to extract + and their corresponding indices. + :rtype: iter(tuple(str)) + """ + current_chunk = [] + current_chunk_position = [] + for idx, word_pos in enumerate(tagged_sent): + word, pos = word_pos + if "-" + chunk_type in pos: # Append the word to the current_chunk. + current_chunk.append(word) + current_chunk_position.append(idx) + else: + if current_chunk: # Flush the full chunk when out of an NP. + _chunk_str = " ".join(current_chunk) + _chunk_pos_str = "-".join(map(str, current_chunk_position)) + yield _chunk_str, _chunk_pos_str + current_chunk = [] + current_chunk_position = [] + if current_chunk: # Flush the last chunk. + yield " ".join(current_chunk), "-".join(map(str, current_chunk_position)) + + +class SennaNERTagger(Senna): + def __init__(self, path, encoding="utf-8"): + super().__init__(path, ["ner"], encoding) + + def tag_sents(self, sentences): + """ + Applies the tag method over a list of sentences. This method will return + for each sentence a list of tuples of (word, tag). + """ + tagged_sents = super().tag_sents(sentences) + for i in range(len(tagged_sents)): + for j in range(len(tagged_sents[i])): + annotations = tagged_sents[i][j] + tagged_sents[i][j] = (annotations["word"], annotations["ner"]) + return tagged_sents diff --git a/nltk/tag/sequential.py b/nltk/tag/sequential.py new file mode 100644 index 0000000..fc62756 --- /dev/null +++ b/nltk/tag/sequential.py @@ -0,0 +1,752 @@ +# Natural Language Toolkit: Sequential Backoff Taggers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird (minor additions) +# Tiago Tresoldi (original affix tagger) +# URL: +# For license information, see LICENSE.TXT + +""" +Classes for tagging sentences sequentially, left to right. The +abstract base class SequentialBackoffTagger serves as the base +class for all the taggers in this module. Tagging of individual words +is performed by the method ``choose_tag()``, which is defined by +subclasses of SequentialBackoffTagger. If a tagger is unable to +determine a tag for the specified token, then its backoff tagger is +consulted instead. Any SequentialBackoffTagger may serve as a +backoff tagger for any other SequentialBackoffTagger. +""" +import ast +import re +from abc import abstractmethod +from typing import List, Optional, Tuple + +from nltk import jsontags +from nltk.classify import NaiveBayesClassifier +from nltk.probability import ConditionalFreqDist +from nltk.tag.api import FeaturesetTaggerI, TaggerI + + +###################################################################### +# Abstract Base Classes +###################################################################### +class SequentialBackoffTagger(TaggerI): + """ + An abstract base class for taggers that tags words sequentially, + left to right. Tagging of individual words is performed by the + ``choose_tag()`` method, which should be defined by subclasses. If + a tagger is unable to determine a tag for the specified token, + then its backoff tagger is consulted. + + :ivar _taggers: A list of all the taggers that should be tried to + tag a token (i.e., self and its backoff taggers). + """ + + def __init__(self, backoff=None): + if backoff is None: + self._taggers = [self] + else: + self._taggers = [self] + backoff._taggers + + @property + def backoff(self): + """The backoff tagger for this tagger.""" + return self._taggers[1] if len(self._taggers) > 1 else None + + def tag(self, tokens): + # docs inherited from TaggerI + tags = [] + for i in range(len(tokens)): + tags.append(self.tag_one(tokens, i, tags)) + return list(zip(tokens, tags)) + + def tag_one(self, tokens, index, history): + """ + Determine an appropriate tag for the specified token, and + return that tag. If this tagger is unable to determine a tag + for the specified token, then its backoff tagger is consulted. + + :rtype: str + :type tokens: list + :param tokens: The list of words that are being tagged. + :type index: int + :param index: The index of the word whose tag should be + returned. + :type history: list(str) + :param history: A list of the tags for all words before *index*. + """ + tag = None + for tagger in self._taggers: + tag = tagger.choose_tag(tokens, index, history) + if tag is not None: + break + return tag + + @abstractmethod + def choose_tag(self, tokens, index, history): + """ + Decide which tag should be used for the specified token, and + return that tag. If this tagger is unable to determine a tag + for the specified token, return None -- do not consult + the backoff tagger. This method should be overridden by + subclasses of SequentialBackoffTagger. + + :rtype: str + :type tokens: list + :param tokens: The list of words that are being tagged. + :type index: int + :param index: The index of the word whose tag should be + returned. + :type history: list(str) + :param history: A list of the tags for all words before *index*. + """ + + +class ContextTagger(SequentialBackoffTagger): + """ + An abstract base class for sequential backoff taggers that choose + a tag for a token based on the value of its "context". Different + subclasses are used to define different contexts. + + A ContextTagger chooses the tag for a token by calculating the + token's context, and looking up the corresponding tag in a table. + This table can be constructed manually; or it can be automatically + constructed based on a training corpus, using the ``_train()`` + factory method. + + :ivar _context_to_tag: Dictionary mapping contexts to tags. + """ + + def __init__(self, context_to_tag, backoff=None): + """ + :param context_to_tag: A dictionary mapping contexts to tags. + :param backoff: The backoff tagger that should be used for this tagger. + """ + super().__init__(backoff) + self._context_to_tag = context_to_tag if context_to_tag else {} + + @abstractmethod + def context(self, tokens, index, history): + """ + :return: the context that should be used to look up the tag + for the specified token; or None if the specified token + should not be handled by this tagger. + :rtype: (hashable) + """ + + def choose_tag(self, tokens, index, history): + context = self.context(tokens, index, history) + return self._context_to_tag.get(context) + + def size(self): + """ + :return: The number of entries in the table used by this + tagger to map from contexts to tags. + """ + return len(self._context_to_tag) + + def __repr__(self): + return f"<{self.__class__.__name__}: size={self.size()}>" + + def _train(self, tagged_corpus, cutoff=0, verbose=False): + """ + Initialize this ContextTagger's ``_context_to_tag`` table + based on the given training data. In particular, for each + context ``c`` in the training data, set + ``_context_to_tag[c]`` to the most frequent tag for that + context. However, exclude any contexts that are already + tagged perfectly by the backoff tagger(s). + + The old value of ``self._context_to_tag`` (if any) is discarded. + + :param tagged_corpus: A tagged corpus. Each item should be + a list of (word, tag tuples. + :param cutoff: If the most likely tag for a context occurs + fewer than cutoff times, then exclude it from the + context-to-tag table for the new tagger. + """ + + token_count = hit_count = 0 + + # A context is considered 'useful' if it's not already tagged + # perfectly by the backoff tagger. + useful_contexts = set() + + # Count how many times each tag occurs in each context. + fd = ConditionalFreqDist() + for sentence in tagged_corpus: + tokens, tags = zip(*sentence) + for index, (token, tag) in enumerate(sentence): + # Record the event. + token_count += 1 + context = self.context(tokens, index, tags[:index]) + if context is None: + continue + fd[context][tag] += 1 + # If the backoff got it wrong, this context is useful: + if self.backoff is None or tag != self.backoff.tag_one( + tokens, index, tags[:index] + ): + useful_contexts.add(context) + + # Build the context_to_tag table -- for each context, figure + # out what the most likely tag is. Only include contexts that + # we've seen at least `cutoff` times. + for context in useful_contexts: + best_tag = fd[context].max() + hits = fd[context][best_tag] + if hits > cutoff: + self._context_to_tag[context] = best_tag + hit_count += hits + + # Display some stats, if requested. + if verbose: + size = len(self._context_to_tag) + backoff = 100 - (hit_count * 100.0) / token_count + pruning = 100 - (size * 100.0) / len(fd.conditions()) + print("[Trained Unigram tagger:", end=" ") + print( + "size={}, backoff={:.2f}%, pruning={:.2f}%]".format( + size, backoff, pruning + ) + ) + + +###################################################################### +# Tagger Classes +###################################################################### + + +@jsontags.register_tag +class DefaultTagger(SequentialBackoffTagger): + """ + A tagger that assigns the same tag to every token. + + >>> from nltk.tag import DefaultTagger + >>> default_tagger = DefaultTagger('NN') + >>> list(default_tagger.tag('This is a test'.split())) + [('This', 'NN'), ('is', 'NN'), ('a', 'NN'), ('test', 'NN')] + + This tagger is recommended as a backoff tagger, in cases where + a more powerful tagger is unable to assign a tag to the word + (e.g. because the word was not seen during training). + + :param tag: The tag to assign to each token + :type tag: str + """ + + json_tag = "nltk.tag.sequential.DefaultTagger" + + def __init__(self, tag): + self._tag = tag + super().__init__(None) + + def encode_json_obj(self): + return self._tag + + @classmethod + def decode_json_obj(cls, obj): + tag = obj + return cls(tag) + + def choose_tag(self, tokens, index, history): + return self._tag # ignore token and history + + def __repr__(self): + return f"" + + +@jsontags.register_tag +class NgramTagger(ContextTagger): + """ + A tagger that chooses a token's tag based on its word string and + on the preceding n word's tags. In particular, a tuple + (tags[i-n:i-1], words[i]) is looked up in a table, and the + corresponding tag is returned. N-gram taggers are typically + trained on a tagged corpus. + + Train a new NgramTagger using the given training data or + the supplied model. In particular, construct a new tagger + whose table maps from each context (tag[i-n:i-1], word[i]) + to the most frequent tag for that context. But exclude any + contexts that are already tagged perfectly by the backoff + tagger. + + :param train: A tagged corpus consisting of a list of tagged + sentences, where each sentence is a list of (word, tag) tuples. + :param backoff: A backoff tagger, to be used by the new + tagger if it encounters an unknown context. + :param cutoff: If the most likely tag for a context occurs + fewer than *cutoff* times, then exclude it from the + context-to-tag table for the new tagger. + """ + + json_tag = "nltk.tag.sequential.NgramTagger" + + def __init__( + self, n, train=None, model=None, backoff=None, cutoff=0, verbose=False + ): + self._n = n + self._check_params(train, model) + + super().__init__(model, backoff) + + if train: + self._train(train, cutoff, verbose) + + def encode_json_obj(self): + _context_to_tag = {repr(k): v for k, v in self._context_to_tag.items()} + if "NgramTagger" in self.__class__.__name__: + return self._n, _context_to_tag, self.backoff + else: + return _context_to_tag, self.backoff + + @classmethod + def decode_json_obj(cls, obj): + try: + _n, _context_to_tag, backoff = obj + except ValueError: + _context_to_tag, backoff = obj + + if not _context_to_tag: + return backoff + + _context_to_tag = {ast.literal_eval(k): v for k, v in _context_to_tag.items()} + + if "NgramTagger" in cls.__name__: + return cls(_n, model=_context_to_tag, backoff=backoff) + else: + return cls(model=_context_to_tag, backoff=backoff) + + def context(self, tokens, index, history): + tag_context = tuple(history[max(0, index - self._n + 1) : index]) + return tag_context, tokens[index] + + +@jsontags.register_tag +class UnigramTagger(NgramTagger): + """ + Unigram Tagger + + The UnigramTagger finds the most likely tag for each word in a training + corpus, and then uses that information to assign tags to new tokens. + + >>> from nltk.corpus import brown + >>> from nltk.tag import UnigramTagger + >>> test_sent = brown.sents(categories='news')[0] + >>> unigram_tagger = UnigramTagger(brown.tagged_sents(categories='news')[:500]) + >>> for tok, tag in unigram_tagger.tag(test_sent): + ... print("({}, {}), ".format(tok, tag)) # doctest: +NORMALIZE_WHITESPACE + (The, AT), (Fulton, NP-TL), (County, NN-TL), (Grand, JJ-TL), + (Jury, NN-TL), (said, VBD), (Friday, NR), (an, AT), + (investigation, NN), (of, IN), (Atlanta's, NP$), (recent, JJ), + (primary, NN), (election, NN), (produced, VBD), (``, ``), + (no, AT), (evidence, NN), ('', ''), (that, CS), (any, DTI), + (irregularities, NNS), (took, VBD), (place, NN), (., .), + + :param train: The corpus of training data, a list of tagged sentences + :type train: list(list(tuple(str, str))) + :param model: The tagger model + :type model: dict + :param backoff: Another tagger which this tagger will consult when it is + unable to tag a word + :type backoff: TaggerI + :param cutoff: The number of instances of training data the tagger must see + in order not to use the backoff tagger + :type cutoff: int + """ + + json_tag = "nltk.tag.sequential.UnigramTagger" + + def __init__(self, train=None, model=None, backoff=None, cutoff=0, verbose=False): + super().__init__(1, train, model, backoff, cutoff, verbose) + + def context(self, tokens, index, history): + return tokens[index] + + +@jsontags.register_tag +class BigramTagger(NgramTagger): + """ + A tagger that chooses a token's tag based its word string and on + the preceding words' tag. In particular, a tuple consisting + of the previous tag and the word is looked up in a table, and + the corresponding tag is returned. + + :param train: The corpus of training data, a list of tagged sentences + :type train: list(list(tuple(str, str))) + :param model: The tagger model + :type model: dict + :param backoff: Another tagger which this tagger will consult when it is + unable to tag a word + :type backoff: TaggerI + :param cutoff: The number of instances of training data the tagger must see + in order not to use the backoff tagger + :type cutoff: int + """ + + json_tag = "nltk.tag.sequential.BigramTagger" + + def __init__(self, train=None, model=None, backoff=None, cutoff=0, verbose=False): + super().__init__(2, train, model, backoff, cutoff, verbose) + + +@jsontags.register_tag +class TrigramTagger(NgramTagger): + """ + A tagger that chooses a token's tag based its word string and on + the preceding two words' tags. In particular, a tuple consisting + of the previous two tags and the word is looked up in a table, and + the corresponding tag is returned. + + :param train: The corpus of training data, a list of tagged sentences + :type train: list(list(tuple(str, str))) + :param model: The tagger model + :type model: dict + :param backoff: Another tagger which this tagger will consult when it is + unable to tag a word + :type backoff: TaggerI + :param cutoff: The number of instances of training data the tagger must see + in order not to use the backoff tagger + :type cutoff: int + """ + + json_tag = "nltk.tag.sequential.TrigramTagger" + + def __init__(self, train=None, model=None, backoff=None, cutoff=0, verbose=False): + super().__init__(3, train, model, backoff, cutoff, verbose) + + +@jsontags.register_tag +class AffixTagger(ContextTagger): + """ + A tagger that chooses a token's tag based on a leading or trailing + substring of its word string. (It is important to note that these + substrings are not necessarily "true" morphological affixes). In + particular, a fixed-length substring of the word is looked up in a + table, and the corresponding tag is returned. Affix taggers are + typically constructed by training them on a tagged corpus. + + Construct a new affix tagger. + + :param affix_length: The length of the affixes that should be + considered during training and tagging. Use negative + numbers for suffixes. + :param min_stem_length: Any words whose length is less than + min_stem_length+abs(affix_length) will be assigned a + tag of None by this tagger. + """ + + json_tag = "nltk.tag.sequential.AffixTagger" + + def __init__( + self, + train=None, + model=None, + affix_length=-3, + min_stem_length=2, + backoff=None, + cutoff=0, + verbose=False, + ): + self._check_params(train, model) + + super().__init__(model, backoff) + + self._affix_length = affix_length + self._min_word_length = min_stem_length + abs(affix_length) + + if train: + self._train(train, cutoff, verbose) + + def encode_json_obj(self): + return ( + self._affix_length, + self._min_word_length, + self._context_to_tag, + self.backoff, + ) + + @classmethod + def decode_json_obj(cls, obj): + _affix_length, _min_word_length, _context_to_tag, backoff = obj + return cls( + affix_length=_affix_length, + min_stem_length=_min_word_length - abs(_affix_length), + model=_context_to_tag, + backoff=backoff, + ) + + def context(self, tokens, index, history): + token = tokens[index] + if len(token) < self._min_word_length: + return None + elif self._affix_length > 0: + return token[: self._affix_length] + else: + return token[self._affix_length :] + + +@jsontags.register_tag +class RegexpTagger(SequentialBackoffTagger): + r""" + Regular Expression Tagger + + The RegexpTagger assigns tags to tokens by comparing their + word strings to a series of regular expressions. The following tagger + uses word suffixes to make guesses about the correct Brown Corpus part + of speech tag: + + >>> from nltk.corpus import brown + >>> from nltk.tag import RegexpTagger + >>> test_sent = brown.sents(categories='news')[0] + >>> regexp_tagger = RegexpTagger( + ... [(r'^-?[0-9]+(\.[0-9]+)?$', 'CD'), # cardinal numbers + ... (r'(The|the|A|a|An|an)$', 'AT'), # articles + ... (r'.*able$', 'JJ'), # adjectives + ... (r'.*ness$', 'NN'), # nouns formed from adjectives + ... (r'.*ly$', 'RB'), # adverbs + ... (r'.*s$', 'NNS'), # plural nouns + ... (r'.*ing$', 'VBG'), # gerunds + ... (r'.*ed$', 'VBD'), # past tense verbs + ... (r'.*', 'NN') # nouns (default) + ... ]) + >>> regexp_tagger + + >>> regexp_tagger.tag(test_sent) # doctest: +NORMALIZE_WHITESPACE + [('The', 'AT'), ('Fulton', 'NN'), ('County', 'NN'), ('Grand', 'NN'), ('Jury', 'NN'), + ('said', 'NN'), ('Friday', 'NN'), ('an', 'AT'), ('investigation', 'NN'), ('of', 'NN'), + ("Atlanta's", 'NNS'), ('recent', 'NN'), ('primary', 'NN'), ('election', 'NN'), + ('produced', 'VBD'), ('``', 'NN'), ('no', 'NN'), ('evidence', 'NN'), ("''", 'NN'), + ('that', 'NN'), ('any', 'NN'), ('irregularities', 'NNS'), ('took', 'NN'), + ('place', 'NN'), ('.', 'NN')] + + :type regexps: list(tuple(str, str)) + :param regexps: A list of ``(regexp, tag)`` pairs, each of + which indicates that a word matching ``regexp`` should + be tagged with ``tag``. The pairs will be evaluated in + order. If none of the regexps match a word, then the + optional backoff tagger is invoked, else it is + assigned the tag None. + """ + + json_tag = "nltk.tag.sequential.RegexpTagger" + + def __init__(self, regexps: list[tuple[str, str]], backoff: TaggerI | None = None): + super().__init__(backoff) + self._regexps = [] + for regexp, tag in regexps: + try: + self._regexps.append((re.compile(regexp), tag)) + except Exception as e: + raise Exception( + f"Invalid RegexpTagger regexp: {e}\n- regexp: {regexp!r}\n- tag: {tag!r}" + ) from e + + def encode_json_obj(self): + return [(regexp.pattern, tag) for regexp, tag in self._regexps], self.backoff + + @classmethod + def decode_json_obj(cls, obj): + regexps, backoff = obj + return cls(regexps, backoff) + + def choose_tag(self, tokens, index, history): + for regexp, tag in self._regexps: + if re.match(regexp, tokens[index]): + return tag + return None + + def __repr__(self): + return f"" + + +class ClassifierBasedTagger(SequentialBackoffTagger, FeaturesetTaggerI): + """ + A sequential tagger that uses a classifier to choose the tag for + each token in a sentence. The featureset input for the classifier + is generated by a feature detector function:: + + feature_detector(tokens, index, history) -> featureset + + Where tokens is the list of unlabeled tokens in the sentence; + index is the index of the token for which feature detection + should be performed; and history is list of the tags for all + tokens before index. + + Construct a new classifier-based sequential tagger. + + :param feature_detector: A function used to generate the + featureset input for the classifier:: + feature_detector(tokens, index, history) -> featureset + + :param train: A tagged corpus consisting of a list of tagged + sentences, where each sentence is a list of (word, tag) tuples. + + :param backoff: A backoff tagger, to be used by the new tagger + if it encounters an unknown context. + + :param classifier_builder: A function used to train a new + classifier based on the data in *train*. It should take + one argument, a list of labeled featuresets (i.e., + (featureset, label) tuples). + + :param classifier: The classifier that should be used by the + tagger. This is only useful if you want to manually + construct the classifier; normally, you would use *train* + instead. + + :param backoff: A backoff tagger, used if this tagger is + unable to determine a tag for a given token. + + :param cutoff_prob: If specified, then this tagger will fall + back on its backoff tagger if the probability of the most + likely tag is less than *cutoff_prob*. + """ + + def __init__( + self, + feature_detector=None, + train=None, + classifier_builder=NaiveBayesClassifier.train, + classifier=None, + backoff=None, + cutoff_prob=None, + verbose=False, + ): + self._check_params(train, classifier) + + super().__init__(backoff) + + if (train and classifier) or (not train and not classifier): + raise ValueError( + "Must specify either training data or " "trained classifier." + ) + + if feature_detector is not None: + self._feature_detector = feature_detector + # The feature detector function, used to generate a featureset + # or each token: feature_detector(tokens, index, history) -> featureset + + self._cutoff_prob = cutoff_prob + """Cutoff probability for tagging -- if the probability of the + most likely tag is less than this, then use backoff.""" + + self._classifier = classifier + """The classifier used to choose a tag for each token.""" + + if train: + self._train(train, classifier_builder, verbose) + + def choose_tag(self, tokens, index, history): + # Use our feature detector to get the featureset. + featureset = self.feature_detector(tokens, index, history) + + # Use the classifier to pick a tag. If a cutoff probability + # was specified, then check that the tag's probability is + # higher than that cutoff first; otherwise, return None. + if self._cutoff_prob is None: + return self._classifier.classify(featureset) + + pdist = self._classifier.prob_classify(featureset) + tag = pdist.max() + return tag if pdist.prob(tag) >= self._cutoff_prob else None + + def _train(self, tagged_corpus, classifier_builder, verbose): + """ + Build a new classifier, based on the given training data + *tagged_corpus*. + """ + + classifier_corpus = [] + if verbose: + print("Constructing training corpus for classifier.") + + for sentence in tagged_corpus: + history = [] + untagged_sentence, tags = zip(*sentence) + for index in range(len(sentence)): + featureset = self.feature_detector(untagged_sentence, index, history) + classifier_corpus.append((featureset, tags[index])) + history.append(tags[index]) + + if verbose: + print(f"Training classifier ({len(classifier_corpus)} instances)") + self._classifier = classifier_builder(classifier_corpus) + + def __repr__(self): + return f"" + + def feature_detector(self, tokens, index, history): + """ + Return the feature detector that this tagger uses to generate + featuresets for its classifier. The feature detector is a + function with the signature:: + + feature_detector(tokens, index, history) -> featureset + + See ``classifier()`` + """ + return self._feature_detector(tokens, index, history) + + def classifier(self): + """ + Return the classifier that this tagger uses to choose a tag + for each word in a sentence. The input for this classifier is + generated using this tagger's feature detector. + See ``feature_detector()`` + """ + return self._classifier + + +class ClassifierBasedPOSTagger(ClassifierBasedTagger): + """ + A classifier based part of speech tagger. + """ + + def feature_detector(self, tokens, index, history): + word = tokens[index] + if index == 0: + prevword = prevprevword = None + prevtag = prevprevtag = None + elif index == 1: + prevword = tokens[index - 1].lower() + prevprevword = None + prevtag = history[index - 1] + prevprevtag = None + else: + prevword = tokens[index - 1].lower() + prevprevword = tokens[index - 2].lower() + prevtag = history[index - 1] + prevprevtag = history[index - 2] + + if re.match(r"[0-9]+(\.[0-9]*)?|[0-9]*\.[0-9]+$", word): + shape = "number" + elif re.match(r"\W+$", word): + shape = "punct" + elif re.match("[A-Z][a-z]+$", word): + shape = "upcase" + elif re.match("[a-z]+$", word): + shape = "downcase" + elif re.match(r"\w+$", word): + shape = "mixedcase" + else: + shape = "other" + + features = { + "prevtag": prevtag, + "prevprevtag": prevprevtag, + "word": word, + "word.lower": word.lower(), + "suffix3": word.lower()[-3:], + "suffix2": word.lower()[-2:], + "suffix1": word.lower()[-1:], + "prevprevword": prevprevword, + "prevword": prevword, + "prevtag+word": f"{prevtag}+{word.lower()}", + "prevprevtag+word": f"{prevprevtag}+{word.lower()}", + "prevword+word": f"{prevword}+{word.lower()}", + "shape": shape, + } + return features diff --git a/nltk/tag/stanford.py b/nltk/tag/stanford.py new file mode 100644 index 0000000..a2c2a21 --- /dev/null +++ b/nltk/tag/stanford.py @@ -0,0 +1,245 @@ +# Natural Language Toolkit: Interface to the Stanford Part-of-speech and Named-Entity Taggers +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Nitin Madnani +# Rami Al-Rfou' +# URL: +# For license information, see LICENSE.TXT + +""" +A module for interfacing with the Stanford taggers. + +Tagger models need to be downloaded from https://nlp.stanford.edu/software +and the STANFORD_MODELS environment variable set (a colon-separated +list of paths). + +For more details see the documentation for StanfordPOSTagger and StanfordNERTagger. +""" + +import os +import tempfile +import warnings +from abc import abstractmethod +from subprocess import PIPE + +from nltk.internals import find_file, find_jar, java +from nltk.tag.api import TaggerI + +_stanford_url = "https://nlp.stanford.edu/software" + + +class StanfordTagger(TaggerI): + """ + An interface to Stanford taggers. Subclasses must define: + + - ``_cmd`` property: A property that returns the command that will be + executed. + - ``_SEPARATOR``: Class constant that represents that character that + is used to separate the tokens from their tags. + - ``_JAR`` file: Class constant that represents the jar file name. + """ + + _SEPARATOR = "" + _JAR = "" + + def __init__( + self, + model_filename, + path_to_jar=None, + encoding="utf8", + verbose=False, + java_options="-mx1000m", + ): + # Raise deprecation warning. + warnings.warn( + str( + "\nThe StanfordTokenizer will " + "be deprecated in version 3.2.6.\n" + "Please use \033[91mnltk.parse.corenlp.CoreNLPParser\033[0m instead." + ), + DeprecationWarning, + stacklevel=2, + ) + + if not self._JAR: + warnings.warn( + "The StanfordTagger class is not meant to be " + "instantiated directly. Did you mean " + "StanfordPOSTagger or StanfordNERTagger?" + ) + self._stanford_jar = find_jar( + self._JAR, path_to_jar, searchpath=(), url=_stanford_url, verbose=verbose + ) + + self._stanford_model = find_file( + model_filename, env_vars=("STANFORD_MODELS",), verbose=verbose + ) + + self._encoding = encoding + self.java_options = java_options + + @property + @abstractmethod + def _cmd(self): + """ + A property that returns the command that will be executed. + """ + + def tag(self, tokens): + # This function should return list of tuple rather than list of list + return sum(self.tag_sents([tokens]), []) + + def tag_sents(self, sentences): + encoding = self._encoding + + input_file_path = None + java_succeeded = False + try: + # Create a temporary input file + _input_fh, input_file_path = tempfile.mkstemp(text=True) + self._input_file_path = input_file_path + + cmd = list(self._cmd) + cmd.extend(["-encoding", encoding]) + + # Write the actual sentences to the temporary input file + with os.fdopen(_input_fh, "wb") as input_fh: + _input = "\n".join(" ".join(x) for x in sentences) + if isinstance(_input, str) and encoding: + _input = _input.encode(encoding) + input_fh.write(_input) + + # Run the tagger and get the output + stanpos_output, _stderr = java( + cmd, + classpath=self._stanford_jar, + stdout=PIPE, + stderr=PIPE, + options=self.java_options, + ) + stanpos_output = stanpos_output.decode(encoding) + java_succeeded = True + finally: + if input_file_path: + try: + os.unlink(input_file_path) + except FileNotFoundError: + pass + except OSError: + if java_succeeded: + raise + + return self.parse_output(stanpos_output, sentences) + + def parse_output(self, text, sentences=None): + # Output the tagged sentences + tagged_sentences = [] + for tagged_sentence in text.strip().split("\n"): + sentence = [] + for tagged_word in tagged_sentence.strip().split(): + word_tags = tagged_word.strip().split(self._SEPARATOR) + sentence.append( + ("".join(word_tags[:-1]), word_tags[-1].replace("0", "").upper()) + ) + tagged_sentences.append(sentence) + return tagged_sentences + + +class StanfordPOSTagger(StanfordTagger): + """ + A class for pos tagging with Stanford Tagger. The input is the paths to: + - a model trained on training data + - (optionally) the path to the stanford tagger jar file. If not specified here, + then this jar file must be specified in the CLASSPATH environment variable. + - (optionally) the encoding of the training data (default: UTF-8) + + Example: + + >>> from nltk.tag import StanfordPOSTagger + >>> st = StanfordPOSTagger('english-bidirectional-distsim.tagger') # doctest: +SKIP + >>> st.tag('What is the airspeed of an unladen swallow ?'.split()) # doctest: +SKIP + [('What', 'WP'), ('is', 'VBZ'), ('the', 'DT'), ('airspeed', 'NN'), ('of', 'IN'), ('an', 'DT'), ('unladen', 'JJ'), ('swallow', 'VB'), ('?', '.')] + """ + + _SEPARATOR = "_" + _JAR = "stanford-postagger.jar" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @property + def _cmd(self): + return [ + "edu.stanford.nlp.tagger.maxent.MaxentTagger", + "-model", + self._stanford_model, + "-textFile", + self._input_file_path, + "-tokenize", + "false", + "-outputFormatOptions", + "keepEmptySentences", + ] + + +class StanfordNERTagger(StanfordTagger): + """ + A class for Named-Entity Tagging with Stanford Tagger. The input is the paths to: + + - a model trained on training data + - (optionally) the path to the stanford tagger jar file. If not specified here, + then this jar file must be specified in the CLASSPATH environment variable. + - (optionally) the encoding of the training data (default: UTF-8) + + Example: + + >>> from nltk.tag import StanfordNERTagger + >>> st = StanfordNERTagger('english.all.3class.distsim.crf.ser.gz') # doctest: +SKIP + >>> st.tag('Rami Eid is studying at Stony Brook University in NY'.split()) # doctest: +SKIP + [('Rami', 'PERSON'), ('Eid', 'PERSON'), ('is', 'O'), ('studying', 'O'), + ('at', 'O'), ('Stony', 'ORGANIZATION'), ('Brook', 'ORGANIZATION'), + ('University', 'ORGANIZATION'), ('in', 'O'), ('NY', 'LOCATION')] + """ + + _SEPARATOR = "/" + _JAR = "stanford-ner.jar" + _FORMAT = "slashTags" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @property + def _cmd(self): + # Adding -tokenizerFactory edu.stanford.nlp.process.WhitespaceTokenizer -tokenizerOptions tokenizeNLs=false for not using stanford Tokenizer + return [ + "edu.stanford.nlp.ie.crf.CRFClassifier", + "-loadClassifier", + self._stanford_model, + "-textFile", + self._input_file_path, + "-outputFormat", + self._FORMAT, + "-tokenizerFactory", + "edu.stanford.nlp.process.WhitespaceTokenizer", + "-tokenizerOptions", + '"tokenizeNLs=false"', + ] + + def parse_output(self, text, sentences): + if self._FORMAT == "slashTags": + # Joint together to a big list + tagged_sentences = [] + for tagged_sentence in text.strip().split("\n"): + for tagged_word in tagged_sentence.strip().split(): + word_tags = tagged_word.strip().split(self._SEPARATOR) + tagged_sentences.append(("".join(word_tags[:-1]), word_tags[-1])) + + # Separate it according to the input + result = [] + start = 0 + for sent in sentences: + result.append(tagged_sentences[start : start + len(sent)]) + start += len(sent) + return result + + raise NotImplementedError diff --git a/nltk/tag/tnt.py b/nltk/tag/tnt.py new file mode 100755 index 0000000..f252e9f --- /dev/null +++ b/nltk/tag/tnt.py @@ -0,0 +1,1119 @@ +# Natural Language Toolkit: TnT Tagger +# +# Copyright (C) 2001-2026 NLTK Project +# Author: John Winstead +# Sam Huston +# +# URL: +# For license information, see LICENSE.TXT + +""" +Implementation of 'TnT - A Statistical Part of Speech Tagger' +by Thorsten Brants + +https://aclanthology.org/A00-1031.pdf + +Where Brants (2000) is silent, the implementation makes principled +choices flagged with ``Underspecified by Brants:`` in inline comments +so future readers can tell paper-derived behavior from local policy: + +* ``_compute_lambda`` - even-split tie-breaking when multiple ``c_i`` + reach the max, and zeroed weights on degenerate training input. +* ``train`` - empty sentences contribute no EOS counts. +* ``_expand_states`` - strict ``>`` beam tie-break, so the + first-encountered hypothesis wins. No clearly better policy is + known; future work on deterministic hypothesis recombination + could improve this. +""" + +from math import log2 + +from nltk.probability import ConditionalFreqDist, FreqDist +from nltk.tag.api import TaggerI + +# Used in place of log2(p) when p underflows; log2(1e-300) ~= -996.58 +# is still well above the negative-inf result of log2(0). +_LOG_FLOOR_2 = log2(1e-300) + +# Sentinel tags used at sentence boundaries. +_BOS = ("BOS", False) +_EOS = ("EOS", False) + +_SENT_MARKS = (".", "!", "?", ";") + +# Returned by transition-cache misses so inner-loop ``.get`` calls never +# need an extra ``None`` check before the next lookup. +_EMPTY_DICT: dict = {} + + +def _safe_log2(p): + """``log2(p)`` for a non-zero probability, ``_LOG_FLOOR_2`` otherwise.""" + return log2(p) if p > 1e-300 else _LOG_FLOOR_2 + + +def _safe_inverse(n): + """``1/n`` for a non-zero ``n``, ``0.0`` otherwise so callers can use + multiplication without an inline guard.""" + return (1.0 / n) if n else 0.0 + + +class TnT(TaggerI): + """ + TnT statistical part of speech tagger. + + This class implements the Trigrams'n'Tags tagger described by + Brants (2000). TnT is a second order hidden Markov model. Its + hidden states are part of speech tags, its observations are word + tokens, and each transition is conditioned on the two previous tag + states. + + Input + + TnT estimates and scores sentence boundary transitions, so training + and tagging data should normally be sentence delimited. + + Training data should be a list of tagged sentences. Each sentence + should be a list of ``(word, tag)`` tuples. + + ``tag()`` expects one tokenized sentence. + ``tagdata()`` expects a list of tokenized sentences. + + For simple punctuation based segmentation of token streams, use + ``basic_sent_chop()`` or pass ``segment=True`` to ``tag()``. The + punctuation tokens are ``.``, ``!``, ``?``, and ``;``, following + Brants's sentence boundary heuristic. + + Model + + For a sentence of length ``T``, the decoder chooses the tag sequence + that maximizes + + prod_i P(t_i | t_{i-1}, t_{i-2}) * P(w_i | t_i) + + and then scores an explicit end of sentence transition. EOS is + recorded as an ordinary next state in the trained n-gram stream, + so its factor uses the same deleted interpolation as the body + transitions (unigram, bigram, and trigram tiers). This generalizes + Brants's printed factor ``P(t_{T+1} | t_T)``, which is bigram only. + + Decoding runs in log space over a Viterbi trellis whose state is + the pair ``(t_{i-1}, t_i)``. + + Transition smoothing + + Transition probabilities use context independent deleted + interpolation of unigram, bigram, and trigram tag models. + + P(t_i | t_{i-1}, t_{i-2}) + = l1 * P(t_i) + + l2 * P(t_i | t_{i-1}) + + l3 * P(t_i | t_{i-1}, t_{i-2}) + + The interpolation weights ``l1``, ``l2``, and ``l3`` are estimated + from the training counts by the deleted interpolation procedure in + Brants (2000), section 2.2. + + Emissions + + Known words use a tag dictionary. For a known word, candidate tags + are restricted to the tags assigned to that word in the training + data. The emission probability is the relative frequency of the + word and tag pair in the training lexicon. + + Unknown words use Brants's suffix model by default. The model builds + capitalization specific suffix tries from infrequent training words, + using the threshold of at most 10 occurrences from Brants (2000), + section 2.3. At decode time it uses the longest suffix observed in + training, capped at 10 characters, smooths the suffix tag + distribution by successive abstraction, and applies Bayesian + inversion to obtain emission like scores. + + A user supplied tagger may be passed with ``unk``. When supplied, + it overrides the built in suffix model for unknown words. + + Beam search + + The decoder uses beam pruning to limit memory and runtime. After + each Viterbi step, states whose log probability is worse than the + current best state by more than ``log2(N)`` are discarded. The + default threshold ``N=1000`` follows Brants (2000), section 2.5. + + Capitalization + + When ``C=True``, capitalization is included in the tag state. This + is equivalent to splitting each tag into capitalized and + uncapitalized variants, as described by Brants (2000), section 2.4. + + Reproducibility + + Training-time iteration is sorted in ``_compute_lambda``, + ``_build_suffix_model``, and ``_tagword``'s candidate construction, + so the trained model and decoded output are bit-identical across + reorderings of equivalent training data. + """ + + def __init__(self, unk=None, Trained=False, N=1000, C=False): + """ + Construct a TnT statistical tagger. The tagger must be trained + before it can be used to tag input. + + :param unk: instance of a POS tagger, conforms to TaggerI. + When supplied, overrides the built-in suffix model + on the unknown-word path. + :type unk: TaggerI + :param Trained: Indication that the POS tagger is trained or not. + Set True to skip training the optional ``unk`` + tagger on the next train() call. + :type Trained: bool + :param N: Beam search pruning threshold. After each Viterbi + step any state whose log-probability is worse than + the best by more than a factor of N is discarded. + Must be a positive integer; 1000 is a good default. + :type N: int + :param C: Capitalization flag. When True, tags are differentiated + by whether the source word is capitalized. This rarely + improves accuracy in practice. + :type C: bool + """ + + # ``bool`` is an ``int`` subclass; the explicit bool check rejects + # True/False, which would otherwise pass the int type check. + if isinstance(N, bool) or not isinstance(N, int) or N < 1: + raise ValueError(f"N must be a positive integer, got {N!r}") + + self._beam_threshold = N + self._use_capitalization = C + self._unk = unk + self._unk_trained = Trained + + self._tag_unigrams = FreqDist() + self._tag_bigrams = ConditionalFreqDist() + self._tag_trigrams = ConditionalFreqDist() + self._word_tag_freqs = ConditionalFreqDist() + + self._lambda1 = 0.0 + self._lambda2 = 0.0 + self._lambda3 = 0.0 + self._num_tag_tokens = 0 + self._log2_beam_threshold = 0.0 + + # Unknown-word decoding uses a capitalization split suffix model, a + # raw tag prior, and theta for successive abstraction smoothing. + self._suffix_trie_by_cap = { + False: ConditionalFreqDist(), + True: ConditionalFreqDist(), + } + self._tag_prior_probs = {} + self._theta = 0.0 + + # Read by ``_expand_states`` instead of recomputing the deleted + # interpolation and ``log2`` per beam expansion. + self._trans_logp_unigram = {} + self._trans_logp_bigram = {} + self._trans_logp_trigram = {} + + # Cleared on every train() so contents always reflect current model state. + self._candidate_tags_cache = {} + + self.unknown = 0 + self.known = 0 + + def train(self, data): + """ + Trains the tagger on a list of tagged sentences. Each call + rebuilds the model from scratch on the supplied data. The + n-gram counts, word-tag lexicon, suffix model, and + deleted-interpolation weights are all replaced, and the decode + cache is cleared. + + The optional external unknown-word tagger (``unk``) is trained + on the supplied data only the first time ``train()`` is called. + Subsequent calls leave it alone, since retraining it on each + new training set is rarely what callers want. + + :param data: list of lists of (word, tag) tuples + :type data: list[list[tuple[str, str]]] + """ + + # These structures accumulate corpus statistics, so retraining must + # rebuild them from scratch rather than layer new counts on top. + self._candidate_tags_cache.clear() + self._tag_unigrams = FreqDist() + self._tag_bigrams = ConditionalFreqDist() + self._tag_trigrams = ConditionalFreqDist() + self._word_tag_freqs = ConditionalFreqDist() + + unk = self._unk + if unk is not None and not self._unk_trained: + unk.train(data) + + word_tag_freqs = self._word_tag_freqs + tag_unigrams = self._tag_unigrams + tag_bigrams = self._tag_bigrams + tag_trigrams = self._tag_trigrams + cap_on = self._use_capitalization + + for sent in data: + state_i_minus_2 = _BOS + state_i_minus_1 = _BOS + sent_has_tokens = False + + for word, tag in sent: + sent_has_tokens = True + c_i = cap_on and bool(word) and word[0].isupper() + state_i = (tag, c_i) + + word_tag_freqs[word][tag] += 1 + tag_unigrams[state_i] += 1 + tag_bigrams[state_i_minus_1][state_i] += 1 + tag_trigrams[(state_i_minus_2, state_i_minus_1)][state_i] += 1 + + state_i_minus_2, state_i_minus_1 = state_i_minus_1, state_i + + # Underspecified by Brants: EOS is treated as an ordinary next + # state in the n-gram model, but empty sentences are skipped + # so BOS does not acquire EOS as a spurious successor. + if sent_has_tokens: + tag_unigrams[_EOS] += 1 + tag_bigrams[state_i_minus_1][_EOS] += 1 + tag_trigrams[(state_i_minus_2, state_i_minus_1)][_EOS] += 1 + + self._compute_lambda() + + # This total intentionally includes EOS because the unigram model + # and deleted interpolation are estimated over the same event stream. + self._num_tag_tokens = tag_unigrams.N() + self._log2_beam_threshold = log2(self._beam_threshold) + + self._build_transition_logp_cache() + self._build_suffix_model() + + self._unk_trained = True + + def tag(self, tokens, segment=False): + """ + Tag a single sentence. Delegates the actual decode to + `_tagword`, then pairs each chosen tag with its input token. + + When `segment` is True, the input may contain mid-sequence + sentence punctuation [.!?;]. The decoder splits on those + tokens and re-seeds the BOS state for each segment. + The default is False because most NLTK callers pre-segment, + and auto-splitting on `.` would mis-handle abbreviations + like "Mr." in unsegmented input. + + :param tokens: words to tag + :type tokens: list[str] + :param segment: split on [.!?;] and decode each segment with a + fresh BOS state + :type segment: bool + :return: list of `(word, tag)` tuples + """ + # Catch "forgot to tokenize" mistake before it produces garbage tags. + if isinstance(tokens, (str, bytes)): + raise TypeError( + "tag() expects a list of tokens, not a single string or bytes. " + "Tokenize first (e.g. tokens.split() or word_tokenize(s))." + ) + if segment: + return self._tag_segmented(tokens) + if not (sent := list(tokens)): + return [] + return self._pair_decoded(sent, self._tagword(sent)) + + def tagdata(self, data, segment=False): + """ + Tags a list of sentences. Each input sentence is a list of words; + each output sentence is a list of (word, tag) tuples. + + :param data: list of list of words + :type data: list[list[str]] + :param segment: forwarded to ``tag``. Pass True to auto-split + each input on internal [.!?;] punctuation. + :type segment: bool + :return: list of list of (word, tag) tuples + """ + if isinstance(data, (str, bytes)): + raise TypeError( + "tagdata() expects a list of tokenized sentences, " + "not a single string or bytes object." + ) + return [self.tag(sent, segment=segment) for sent in data] + + def _compute_lambda(self): + """ + Computes the deleted-interpolation weights l1, l2, l3 from the + observed tag n-grams. Tied maxima split the trigram count evenly + among the winning lambdas. Branches with a zero denominator + contribute zero. + + For each trigram (t1, t2, t3) with positive count we compare + + c1 = (f(t3) - 1) / (N - 1) + c2 = (f(t2, t3) - 1) / (f(t2) - 1) + c3 = (f(t1, t2, t3) - 1) / (f(t1, t2) - 1) + """ + + tag_unigrams = self._tag_unigrams + tag_bigrams = self._tag_bigrams + tag_trigrams = self._tag_trigrams + unigram_n_minus_1 = tag_unigrams.N() - 1 + + lambda1_mass = 0.0 + lambda2_mass = 0.0 + lambda3_mass = 0.0 + + # Sorted iteration makes lambda mass accumulation order-independent. + for state_i_minus_2, state_i_minus_1 in sorted(tag_trigrams.conditions()): + trigram_dist = tag_trigrams[(state_i_minus_2, state_i_minus_1)] + bigram_dist = tag_bigrams[state_i_minus_1] + + trigram_n_minus_1 = trigram_dist.N() - 1 + bigram_n_minus_1 = bigram_dist.N() - 1 + + for state_i, count in sorted(trigram_dist.items()): + # Subtracting one leaves the current event out, so each score + # asks which model order would best predict this tag if this + # occurrence were held out. + c1 = ( + (tag_unigrams[state_i] - 1) / unigram_n_minus_1 + if unigram_n_minus_1 + else 0.0 + ) + c2 = ( + (bigram_dist[state_i] - 1) / bigram_n_minus_1 + if bigram_n_minus_1 + else 0.0 + ) + c3 = (count - 1) / trigram_n_minus_1 if trigram_n_minus_1 else 0.0 + + # Underspecified by Brants: the trigram's count is credited + # to the model order with the strongest held-out estimate. + # Splitting ties evenly across the winning lambdas avoids + # introducing an arbitrary preference between orders. + maxc = max(c1, c2, c3) + w1 = c1 == maxc + w2 = c2 == maxc + w3 = c3 == maxc + share = count / (w1 + w2 + w3) + + if w1: + lambda1_mass += share + if w2: + lambda2_mass += share + if w3: + lambda3_mass += share + + # Underspecified by Brants: normalization turns the accumulated + # winning mass into mixture weights. Zeroing on degenerate input + # (no positive trigram mass) avoids a divide-by-zero and prevents + # stale weights from a previous training run from leaking through. + total_mass = lambda1_mass + lambda2_mass + lambda3_mass + if total_mass > 0: + self._lambda1 = lambda1_mass / total_mass + self._lambda2 = lambda2_mass / total_mass + self._lambda3 = lambda3_mass / total_mass + else: + self._lambda1 = 0.0 + self._lambda2 = 0.0 + self._lambda3 = 0.0 + + def _build_transition_logp_cache(self): + """ + Precompute transition log probabilities for Viterbi expansion. + + The decoder scores each transition with the same deleted + interpolation used by the model. + + P(t_i | t_{i-2}, t_{i-1}) + = l1 P(t_i) + + l2 P(t_i | t_{i-1}) + + l3 P(t_i | t_{i-2}, t_{i-1}) + + Computing this value inside the beam loop repeats the same work + many times, so training builds caches for the observed unigram, + bigram, and trigram contexts. During decoding, ``_expand_states`` + first tries the trigram cache, then the bigram cache, then the + unigram cache. + + The caches store log2 probabilities. + + ``_trans_logp_unigram[state]`` + ``_trans_logp_bigram[prev1][state]`` + ``_trans_logp_trigram[(prev2, prev1)][state]`` + + Bigram cache entries include the unigram and bigram interpolation + terms. Trigram cache entries include all three terms. The + parenthesization in the probability calculation is kept stable so + cached values match the previous per-call computation. + """ + tag_unigrams = self._tag_unigrams + tag_bigrams = self._tag_bigrams + tag_trigrams = self._tag_trigrams + + lambda1, lambda2, lambda3 = self._lambda1, self._lambda2, self._lambda3 + inv_total_N = _safe_inverse(self._num_tag_tokens) + + # Computed once and reused below so the bigram and trigram caches + # don't recompute the unigram contribution per (history, current). + unigram_part = { + state: lambda1 * (count * inv_total_N) + for state, count in tag_unigrams.items() + } + self._trans_logp_unigram = { + state: _safe_log2(p) for state, p in unigram_part.items() + } + + bigram_logp = {} + for prev1 in tag_bigrams.conditions(): + bigram_dist = tag_bigrams[prev1] + inv_bigram_N = _safe_inverse(bigram_dist.N()) + + bigram_logp[prev1] = { + current: _safe_log2( + unigram_part.get(current, 0.0) + lambda2 * count * inv_bigram_N + ) + for current, count in bigram_dist.items() + } + self._trans_logp_bigram = bigram_logp + + trigram_logp = {} + for prev_pair in tag_trigrams.conditions(): + _, prev1 = prev_pair + + trigram_dist = tag_trigrams[prev_pair] + bigram_dist = tag_bigrams[prev1] + + inv_trigram_N = _safe_inverse(trigram_dist.N()) + inv_bigram_N = _safe_inverse(bigram_dist.N()) + bigram_get = bigram_dist.get + + trigram_logp[prev_pair] = { + current: _safe_log2( + unigram_part.get(current, 0.0) + + lambda2 * bigram_get(current, 0) * inv_bigram_N + + lambda3 * count * inv_trigram_N + ) + for current, count in trigram_dist.items() + } + self._trans_logp_trigram = trigram_logp + + def _build_suffix_model(self): + """ + Build the suffix model used for unseen words. + + The model stores three decode-time values: capitalization-split + suffix tries, the successive-abstraction weight theta, and raw + tag priors collapsed across capitalization. + + EOS is excluded from the priors because it is a sequence marker, + not a lexical tag. Suffix counts come only from lexicon words + with total count at most 10, following Brants's infrequent-word + threshold for unknown-word modeling. + """ + tag_unigrams = self._tag_unigrams + word_tag_freqs = self._word_tag_freqs + + tag_counts: dict = {} + for (tag, _), count in sorted(tag_unigrams.items()): + # Exclude the EOS sentinel from all capitalization states. + if tag == _EOS[0]: + continue + tag_counts[tag] = tag_counts.get(tag, 0) + count + + total = sum(tag_counts.values()) + if total > 0: + tag_prior_probs = {tag: count / total for tag, count in tag_counts.items()} + else: + tag_prior_probs = {} + + # Theta controls how strongly the suffix recursion is pulled back + # toward the less specific estimate at each abstraction step. + priors = list(tag_prior_probs.values()) + n_priors = len(priors) + if n_priors > 1: + mean = sum(priors) / n_priors + theta = ( + sum((prior - mean) ** 2 for prior in priors) / (n_priors - 1) + ) ** 0.5 + else: + theta = 0.0 + + # Each capitalization bucket stores every suffix length up to 10, + # so decode can walk from the shortest matched ending to the longest. + suffix_trie_by_cap = { + False: ConditionalFreqDist(), + True: ConditionalFreqDist(), + } + + # Sorted iteration makes suffix-trie insertion order-independent. + for word in sorted(word_tag_freqs.conditions()): + tag_freqs = word_tag_freqs[word] + if not word or tag_freqs.N() > 10: + continue + + suffix_trie = suffix_trie_by_cap[word[0].isupper()] + max_suffix_len = min(len(word), 10) + + for m in range(1, max_suffix_len + 1): + suffix_dist = suffix_trie[word[-m:]] + for tag, count in sorted(tag_freqs.items()): + suffix_dist[tag] += count + + self._tag_prior_probs = tag_prior_probs + self._theta = theta + self._suffix_trie_by_cap = suffix_trie_by_cap + + def _unknown_tag_scores(self, word): + """ + Score candidate tags for an unknown word using Brants's suffix + model. + + The intuition is that a word's last few characters predict its + tag well, since `-able` words tend to be adjectives, `-ing` + words tend to be participles, and so on. Starting from the + unigram tag prior, we walk one suffix character at a time up + to the longest suffix we saw during training (capped at 10 + characters), blending each suffix's tag distribution into the + running estimate via the successive abstraction recursion + + P(t | l_{n-i+1}...l_n) = (P_hat + theta * P_prev) / (1 + theta) + + If the word's tail is unfamiliar, the recursion never gets past + the prior, which is the back-off case. + + :return: Bayes-inverted scores. Each raw tag maps to a quantity + proportional to P(suffix | t). The P(suffix) constant + drops out because it does not depend on the tag, which + preserves the argmax without computing it. Tags with + zero prior are omitted. + """ + tag_priors = self._tag_prior_probs + if not tag_priors: + return {} + + is_capitalized = bool(word) and word[0].isupper() + suffix_trie = self._suffix_trie_by_cap[is_capitalized] + max_suffix_len = min(len(word), 10) + + # The trie contains every suffix length up to the cutoff. Once the + # longest matching suffix is found, all shorter suffixes are also + # available for successive abstraction. + longest = 0 + for m in range(max_suffix_len, 0, -1): + if word[-m:] in suffix_trie: + longest = m + break + + # No matched suffix means the estimate stays at the unigram prior. + # After Bayes inversion, every tag then receives the same score. + if longest == 0: + return {tag: 1.0 for tag, prior in tag_priors.items() if prior > 0} + + theta = self._theta + + # With theta equal to zero there is no smoothing, so the estimate + # is the empirical distribution of the longest matched suffix. + # Tags absent from that bucket would have score zero, so they are + # dropped from the result instead of being emitted as floor-only + # candidates that the beam would prune anyway. + if theta == 0.0: + suffix_dist = suffix_trie[word[-longest:]] + inv_suffix_N = 1.0 / suffix_dist.N() + return { + tag: (count * inv_suffix_N) / tag_priors[tag] + for tag, count in suffix_dist.items() + if tag_priors.get(tag, 0) > 0 + } + + # Dense successive abstraction updates every tag at every suffix + # length. For tags absent from the current suffix bucket, that update + # is the same shared shrinkage. Factor that shared term into one + # scalar, and keep only tag specific corrections in delta. + denom = 1.0 + theta + miss_scale = theta / denom + + global_scale = 1.0 + delta: dict = {} + + for i in range(1, longest + 1): + suffix_dist = suffix_trie[word[-i:]] + inv_suffix_N = 1.0 / suffix_dist.N() + + # Apply the shared shrinkage for all tags, then add the suffix + # evidence only for tags observed in this bucket. + global_scale *= miss_scale + corr_scale = inv_suffix_N / (denom * global_scale) + + for tag, count in suffix_dist.items(): + delta[tag] = delta.get(tag, 0.0) + count * corr_scale + + # In the factored form, Bayes inversion becomes + # P(t | suffix) / P(t) = global_scale * (1 + delta[t] / P(t)). + # Untouched tags share one score. Touched tags get a correction + # relative to their unigram prior. + result = {} + for tag, prior in tag_priors.items(): + if prior <= 0: + continue + extra = delta.get(tag) + if extra is None: + result[tag] = global_scale + else: + result[tag] = global_scale * (1.0 + extra / prior) + + return result + + def _tag_segmented(self, tokens): + """ + Tag ``tokens`` as one or more sentences split on ``[.!?;]``. + + Each sentence-final punctuation token stays with the segment it + closes, and a trailing fragment without sentence-final punctuation + is still tagged as its own segment. + """ + tagged: list = [] + segment: list = [] + + sent_marks = _SENT_MARKS + tagword = self._tagword + pair_decoded = self._pair_decoded + extend = tagged.extend + + for token in tokens: + segment.append(token) + if token in sent_marks: + extend(pair_decoded(segment, tagword(segment))) + segment.clear() + + if segment: + extend(pair_decoded(segment, tagword(segment))) + + return tagged + + def _tagword(self, sent): + """ + Tag one sentence with second-order Viterbi decoding. + + The lattice state is the last two tag states, so paths that share + ``(state_{i-1}, state_i)`` are merged immediately. Known words draw + candidates from the lexicon. Unknown words are scored either by the + external ``unk`` tagger or by the suffix model. After each word, the + beam keeps only states whose score is within ``log2(N)`` of the best + surviving path. The decode then scores an explicit EOS transition + using deleted interpolation over the unigram, bigram, and trigram + EOS counts, and walks backpointers to recover the best state + sequence. + + :param sent: words to tag + :type sent: list[str] + :return: list shaped ``[BOS, BOS, state_0, ..., state_{T-1}]`` + where each state is a ``(tag, capitalization)`` pair. + """ + if not sent: + return [_BOS, _BOS] + + T = len(sent) + + # Local bindings keep the hot loop on plain locals rather than + # repeated attribute lookups. + word_tag_freqs = self._word_tag_freqs + tag_unigrams = self._tag_unigrams + trans_logp_unigram_get = self._trans_logp_unigram.get + log2_beam_threshold = self._log2_beam_threshold + cap_on = self._use_capitalization + unk = self._unk + cache = self._candidate_tags_cache + unknown_tag_scores = self._unknown_tag_scores + expand_states = self._expand_states + + # Each level keeps only the best path reaching a given + # ``(state_{i-1}, state_i)`` key. The backpointer stores + # ``state_{i-2}`` so the best path can be reconstructed at the end. + states = {(_BOS, _BOS): (0.0, _BOS)} + state_history = [states] + + candidate_tags: tuple + for word in sent: + c_i = cap_on and bool(word) and word[0].isupper() + tag_freqs = word_tag_freqs.get(word) + + if tag_freqs is not None: + self.known += 1 + else: + self.unknown += 1 + + # External unknown-word taggers are treated as potentially + # stateful. The built-in known-word and suffix-model paths are + # pure given ``(word, c_i)`` and the trained model, so they cache. + if tag_freqs is None and unk is not None: + # Validate length before destructuring so a misbehaving + # external tagger surfaces a clear error instead of a + # cryptic "too many values to unpack" message. + unk_out = list(unk.tag([word])) + if len(unk_out) != 1: + raise ValueError( + f"unk tagger returned {len(unk_out)} tags for 1 word; " + f"expected exactly 1" + ) + ((_word, tag),) = unk_out + state_i = (tag, c_i) + unigram_logp = trans_logp_unigram_get(state_i, _LOG_FLOOR_2) + candidate_tags = ((state_i, 0.0, unigram_logp),) + else: + cache_key = (word, c_i) + cached = cache.get(cache_key) + + if cached is None: + if tag_freqs is not None: + # Known words only consider tags actually seen with + # that surface form. The lexical term is P(word | tag). + entries = [] + for tag, tag_count in sorted(tag_freqs.items()): + state_i = (tag, c_i) + unigram_state_i = tag_unigrams[state_i] + unigram_logp = trans_logp_unigram_get(state_i, _LOG_FLOOR_2) + entries.append( + ( + state_i, + log2(tag_count / unigram_state_i), + unigram_logp, + ) + ) + candidate_tags = tuple(entries) + else: + # Unknown words use the suffix model as their lexical + # score. Bayes inversion turns the suffix posterior into + # the emission-like quantity used by the decoder. + suffix_scores = unknown_tag_scores(word) + + if not suffix_scores: + # An untrained tagger has no suffix priors, so the + # only safe fallback is a literal ``Unk`` state. + state_i = ("Unk", c_i) + unigram_logp = trans_logp_unigram_get(state_i, _LOG_FLOOR_2) + candidate_tags = ((state_i, 0.0, unigram_logp),) + else: + entries = [] + for tag, score in suffix_scores.items(): + state_i = (tag, c_i) + unigram_logp = trans_logp_unigram_get( + state_i, _LOG_FLOOR_2 + ) + entries.append( + (state_i, _safe_log2(score), unigram_logp) + ) + candidate_tags = tuple(entries) + + cache[cache_key] = candidate_tags + else: + candidate_tags = cached + + new_states, best_logp = expand_states(states, candidate_tags) + + # Threshold pruning keeps the beam relative to the best current + # path, which is the pruning rule described in the paper. + cutoff = best_logp - log2_beam_threshold + states = {k: v for k, v in new_states.items() if v[0] >= cutoff} + state_history.append(states) + + # EOS uses raw probabilities (not the logged cache) so the lambdas + # can interpolate across orders before ``log2``. Scored once per + # sentence, so skipping the cache is irrelevant for performance. + tag_bigrams = self._tag_bigrams + tag_trigrams = self._tag_trigrams + lambda1, lambda2, lambda3 = self._lambda1, self._lambda2, self._lambda3 + num_tag_tokens = self._num_tag_tokens + p_eos_unigram = (tag_unigrams[_EOS] / num_tag_tokens) if num_tag_tokens else 0.0 + + best_final_key = next(iter(states)) + best_final_logp = float("-inf") + + for predecessor_key, (prefix_logp, _) in states.items(): + state_i_minus_1 = predecessor_key[1] + + # ``ConditionalFreqDist.__getitem__`` would create empty entries + # for unseen keys, mutating the trained model during decode. Use + # ``.get`` so an unseen history just contributes zero. + bigram_dist = tag_bigrams.get(state_i_minus_1) + if bigram_dist is None: + p_eos_bigram = 0.0 + else: + bigram_N = bigram_dist.N() + p_eos_bigram = (bigram_dist[_EOS] / bigram_N) if bigram_N else 0.0 + + trigram_dist = tag_trigrams.get(predecessor_key) + if trigram_dist is None: + p_eos_trigram = 0.0 + else: + trigram_N = trigram_dist.N() + p_eos_trigram = (trigram_dist[_EOS] / trigram_N) if trigram_N else 0.0 + + p_eos_given_history = ( + lambda1 * p_eos_unigram + + lambda2 * p_eos_bigram + + lambda3 * p_eos_trigram + ) + final_logp = prefix_logp + _safe_log2(p_eos_given_history) + if final_logp > best_final_logp: + best_final_logp = final_logp + best_final_key = predecessor_key + + # Walking the stored ``state_{i-2}`` backpointers recovers the best + # full state sequence from the best final state pair. + states_reversed = [best_final_key[1]] + if T >= 2: + states_reversed.append(best_final_key[0]) + + current_key = best_final_key + for level in range(T, 2, -1): + backpointer = state_history[level][current_key][1] + states_reversed.append(backpointer) + current_key = (backpointer, current_key[0]) + + states_reversed.reverse() + return [_BOS, _BOS] + states_reversed + + def _expand_states(self, states, candidate_tags): + """ + Take one Viterbi step. For every predecessor state, score each + candidate `state_i` from `candidate_tags` and accumulate the + results into a new state dict keyed by + `(state_i_minus_1, state_i)`. When two predecessors land on the + same key, keep the higher-scoring one and discard the other. The + second-order Markov assumption means everything after this point + depends only on the last two states, so the discarded path can + never beat the kept one. Ties (equal `path_logp`) are broken by + keeping the first-encountered path; sorted candidate construction + upstream makes that tie-break deterministic. + + `candidate_tags` is a sequence of `(state_i, log_emit, + unigram_logp)` triples. `log_emit` is the lexical log-probability + for known words, the Bayes-inverted suffix score for unknown + words, or zero for an external unknown-word tagger. + + `unigram_logp` is the precomputed unigram-tier transition + log-probability for `state_i`, used as the fallback when the + trigram and bigram caches both miss. Precomputing it once per + candidate removes a hot lookup from the inner loop on high-OOV + corpora. + + :return: ``(new_states, best_logp)``. ``new_states`` maps + `(state_i_minus_1, state_i)` to + `(logp, state_i_minus_2)`, where `state_i_minus_2` is + the backpointer used to reconstruct the best path after + the final EOS step. `best_logp` is the maximum `logp` + across `new_states`, returned so the caller can apply + threshold pruning without a second pass. + """ + trans_logp_trigram = self._trans_logp_trigram + trans_logp_bigram = self._trans_logp_bigram + + new_states: dict = {} + new_states_get = new_states.get + best_logp = float("-inf") + + for predecessor_key, (prefix_logp, _) in states.items(): + state_i_minus_2, state_i_minus_1 = predecessor_key + + # Per-history caches are constant across the candidate loop; + # resolving them once and using ``_EMPTY_DICT`` for misses + # lets the inner ``.get`` calls run without ``None`` branches. + trigram_logp = trans_logp_trigram.get(predecessor_key, _EMPTY_DICT) + bigram_logp = trans_logp_bigram.get(state_i_minus_1, _EMPTY_DICT) + trigram_logp_get = trigram_logp.get + bigram_logp_get = bigram_logp.get + + for state_i, log_emit, unigram_logp in candidate_tags: + trans_logp = trigram_logp_get(state_i) + if trans_logp is None: + trans_logp = bigram_logp_get(state_i) + if trans_logp is None: + # The candidate tuple carries the unigram fallback, + # including the floor for states never observed as + # unigrams, so the inner loop avoids a third dict lookup. + trans_logp = unigram_logp + + # Parens match the previous ``step_logp + prefix_logp`` + # order so the sum is bit-identical to the old decoder. + path_logp = prefix_logp + (trans_logp + log_emit) + next_state = (state_i_minus_1, state_i) + + # When two paths land on the same (state_{i-1}, state_i) + # key, only the higher-scoring prefix can matter going + # forward: future transitions depend on the key alone. + # + # Underspecified by Brants: on exact ties, strict ``>`` + # keeps the first-encountered hypothesis. Sorted + # candidate construction upstream fixes the iteration + # order, so the tie-break is deterministic across reruns. + # In practice this branch never fires (0 hits in 100k+ + # Treebank comparisons), so ``>`` vs ``>=`` is a no-op. + prev_best = new_states_get(next_state) + if prev_best is None or path_logp > prev_best[0]: + new_states[next_state] = (path_logp, state_i_minus_2) + if path_logp > best_logp: + best_logp = path_logp + + return new_states, best_logp + + @staticmethod + def _pair_decoded(words, states): + """Convert `_tagword` output into ``(word, tag)`` pairs by dropping + the two BOS entries and the capitalization flag from each state.""" + return [(word, states[i + 2][0]) for i, word in enumerate(words)] + + +# ----------------------------- +# Sentence segmentation helpers +# ----------------------------- + + +def basic_sent_chop(data, raw=True, sent_marks=_SENT_MARKS): + """ + Split a flat token sequence into sentence-like segments. + + Splits after tokens whose surface form appears in ``sent_marks``. + Works for raw tokens and tagged ``(word, tag)`` tokens. + + :param data: flat sequence of tokens or ``(word, tag)`` tuples + :param raw: whether ``data`` contains raw tokens + :param sent_marks: token strings that end a sentence segment + :return: list of sentence segments with the same token shape as input + """ + sent_marks = set(sent_marks) + sentences = [] + sentence = [] + + for token in data: + sentence.append(token) + word = token if raw else token[0] + if word in sent_marks: + sentences.append(sentence) + sentence = [] + + if sentence: + sentences.append(sentence) + + return sentences + + +# ------------ +# Demo helpers +# ------------ + + +def _treebank_demo_split(test_size=1000): + """Return train, held-out final Treebank slice, and train vocabulary.""" + from nltk.corpus import treebank + + sents = list(treebank.tagged_sents()) + if not 0 < test_size < len(sents): + raise ValueError( + f"test_size must be between 1 and {len(sents) - 1}, got {test_size!r}" + ) + + cut = len(sents) - test_size + train_sents = sents[:cut] + test_sents = sents[cut:] + train_vocab = {word for sent in train_sents for word, _ in sent} + return train_sents, test_sents, train_vocab + + +def _score_tagger(tagger, test_sents, train_vocab): + """Return overall, seen, and OOV scores for tagged sentences.""" + correct = total = 0 + seen_correct = seen_total = 0 + oov_correct = oov_total = 0 + + for sent in test_sents: + words = [word for word, _ in sent] + gold = [tag for _, tag in sent] + pred = [tag for _, tag in tagger.tag(words)] + + if len(pred) != len(gold): + raise ValueError(f"tagger returned {len(pred)} tags for {len(gold)} tokens") + + for word, guess, truth in zip(words, pred, gold): + total += 1 + hit = guess == truth + if hit: + correct += 1 + + if word in train_vocab: + seen_total += 1 + if hit: + seen_correct += 1 + else: + oov_total += 1 + if hit: + oov_correct += 1 + + return { + "accuracy": correct / total if total else 0.0, + "seen_accuracy": seen_correct / seen_total if seen_total else None, + "oov_accuracy": oov_correct / oov_total if oov_total else None, + "oov_rate": oov_total / total if total else 0.0, + "total": total, + "seen_total": seen_total, + "oov_total": oov_total, + } + + +def _format_score(score): + return "n/a" if score is None else f"{score:.4f}" + + +# ----- +# Demos +# ----- + + +def demo(test_size=1000): + """Evaluate TnT on a held-out Treebank slice.""" + train_sents, test_sents, train_vocab = _treebank_demo_split(test_size) + + for use_capitalization in (False, True): + tagger = TnT(N=1000, C=use_capitalization) + tagger.train(train_sents) + scores = _score_tagger(tagger, test_sents, train_vocab) + + print(f"Capitalization: {use_capitalization}") + print(f"Accuracy: {scores['accuracy']:.4f}") + print(f"Seen accuracy: {_format_score(scores['seen_accuracy'])}") + print(f"OOV accuracy: {_format_score(scores['oov_accuracy'])}") + print(f"OOV rate: {scores['oov_rate']:.4f}") + print() + + +def demo_errors(limit=25, test_size=1000): + """Print the first ``limit`` errors from a held-out Treebank slice.""" + if limit < 1: + raise ValueError(f"limit must be at least 1, got {limit!r}") + + train_sents, test_sents, train_vocab = _treebank_demo_split(test_size) + + tagger = TnT(N=1000, C=True) + tagger.train(train_sents) + + shown = 0 + for sent in test_sents: + words = [word for word, _ in sent] + gold = [tag for _, tag in sent] + pred = [tag for _, tag in tagger.tag(words)] + + if len(pred) != len(gold): + raise ValueError(f"tagger returned {len(pred)} tags for {len(gold)} tokens") + + for word, guess, truth in zip(words, pred, gold): + if guess == truth: + continue + + status = "seen" if word in train_vocab else "OOV" + print(f"{word!r} ({status}): guessed {guess!r}, gold {truth!r}") + shown += 1 + if shown >= limit: + return + + if shown == 0: + print("No errors found.") diff --git a/nltk/tag/util.py b/nltk/tag/util.py new file mode 100644 index 0000000..3a2fecf --- /dev/null +++ b/nltk/tag/util.py @@ -0,0 +1,72 @@ +# Natural Language Toolkit: Tagger Utilities +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# Steven Bird +# URL: +# For license information, see LICENSE.TXT + + +def str2tuple(s, sep="/"): + """ + Given the string representation of a tagged token, return the + corresponding tuple representation. The rightmost occurrence of + *sep* in *s* will be used to divide *s* into a word string and + a tag string. If *sep* does not occur in *s*, return (s, None). + + >>> from nltk.tag.util import str2tuple + >>> str2tuple('fly/NN') + ('fly', 'NN') + + :type s: str + :param s: The string representation of a tagged token. + :type sep: str + :param sep: The separator string used to separate word strings + from tags. + """ + loc = s.rfind(sep) + if loc >= 0: + return (s[:loc], s[loc + len(sep) :].upper()) + else: + return (s, None) + + +def tuple2str(tagged_token, sep="/"): + """ + Given the tuple representation of a tagged token, return the + corresponding string representation. This representation is + formed by concatenating the token's word string, followed by the + separator, followed by the token's tag. (If the tag is None, + then just return the bare word string.) + + >>> from nltk.tag.util import tuple2str + >>> tagged_token = ('fly', 'NN') + >>> tuple2str(tagged_token) + 'fly/NN' + + :type tagged_token: tuple(str, str) + :param tagged_token: The tuple representation of a tagged token. + :type sep: str + :param sep: The separator string used to separate word strings + from tags. + """ + word, tag = tagged_token + if tag is None: + return word + else: + assert sep not in tag, "tag may not contain sep!" + return f"{word}{sep}{tag}" + + +def untag(tagged_sentence): + """ + Given a tagged sentence, return an untagged version of that + sentence. I.e., return a list containing the first element + of each tuple in *tagged_sentence*. + + >>> from nltk.tag.util import untag + >>> untag([('John', 'NNP'), ('saw', 'VBD'), ('Mary', 'NNP')]) + ['John', 'saw', 'Mary'] + + """ + return [w for (w, t) in tagged_sentence] diff --git a/nltk/tbl/__init__.py b/nltk/tbl/__init__.py new file mode 100644 index 0000000..2d4594b --- /dev/null +++ b/nltk/tbl/__init__.py @@ -0,0 +1,31 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +""" +Transformation Based Learning + +A general purpose package for Transformation Based Learning, +currently used by nltk.tag.BrillTagger. + +isort:skip_file +""" + +from nltk.tbl.template import Template + +# API: Template(...), Template.expand(...) + +from nltk.tbl.feature import Feature + +# API: Feature(...), Feature.expand(...) + +from nltk.tbl.rule import Rule + +# API: Rule.format(...), Rule.templatetid + +from nltk.tbl.erroranalysis import error_list diff --git a/nltk/tbl/api.py b/nltk/tbl/api.py new file mode 100644 index 0000000..e69de29 diff --git a/nltk/tbl/demo.py b/nltk/tbl/demo.py new file mode 100644 index 0000000..dc47c39 --- /dev/null +++ b/nltk/tbl/demo.py @@ -0,0 +1,419 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +import os +import pickle +import random +import time + +from nltk.corpus import treebank +from nltk.picklesec import pickle_load +from nltk.tag import BrillTaggerTrainer, RegexpTagger, UnigramTagger +from nltk.tag.brill import Pos, Word +from nltk.tbl import Template, error_list + + +def demo(): + """ + Run a demo with defaults. See source comments for details, + or docstrings of any of the more specific demo_* functions. + """ + postag() + + +def demo_repr_rule_format(): + """ + Exemplify repr(Rule) (see also str(Rule) and Rule.format("verbose")) + """ + postag(ruleformat="repr") + + +def demo_str_rule_format(): + """ + Exemplify repr(Rule) (see also str(Rule) and Rule.format("verbose")) + """ + postag(ruleformat="str") + + +def demo_verbose_rule_format(): + """ + Exemplify Rule.format("verbose") + """ + postag(ruleformat="verbose") + + +def demo_multiposition_feature(): + """ + The feature/s of a template takes a list of positions + relative to the current word where the feature should be + looked for, conceptually joined by logical OR. For instance, + Pos([-1, 1]), given a value V, will hold whenever V is found + one step to the left and/or one step to the right. + + For contiguous ranges, a 2-arg form giving inclusive end + points can also be used: Pos(-3, -1) is the same as the arg + below. + """ + postag(templates=[Template(Pos([-3, -2, -1]))]) + + +def demo_multifeature_template(): + """ + Templates can have more than a single feature. + """ + postag(templates=[Template(Word([0]), Pos([-2, -1]))]) + + +def demo_template_statistics(): + """ + Show aggregate statistics per template. Little used templates are + candidates for deletion, much used templates may possibly be refined. + + Deleting unused templates is mostly about saving time and/or space: + training is basically O(T) in the number of templates T + (also in terms of memory usage, which often will be the limiting factor). + """ + postag(incremental_stats=True, template_stats=True) + + +def demo_generated_templates(): + """ + Template.expand and Feature.expand are class methods facilitating + generating large amounts of templates. See their documentation for + details. + + Note: training with 500 templates can easily fill all available + even on relatively small corpora + """ + wordtpls = Word.expand([-1, 0, 1], [1, 2], excludezero=False) + tagtpls = Pos.expand([-2, -1, 0, 1], [1, 2], excludezero=True) + templates = list(Template.expand([wordtpls, tagtpls], combinations=(1, 3))) + print( + "Generated {} templates for transformation-based learning".format( + len(templates) + ) + ) + postag(templates=templates, incremental_stats=True, template_stats=True) + + +def demo_learning_curve(): + """ + Plot a learning curve -- the contribution on tagging accuracy of + the individual rules. + Note: requires matplotlib + """ + postag( + incremental_stats=True, + separate_baseline_data=True, + learning_curve_output="learningcurve.png", + ) + + +def demo_error_analysis(): + """ + Writes a file with context for each erroneous word after tagging testing data + """ + postag(error_output="errors.txt") + + +def demo_serialize_tagger(): + """ + Serializes the learned tagger to a file in pickle format; reloads it + and validates the process. + """ + postag(serialize_output="tagger.pcl") + + +def demo_high_accuracy_rules(): + """ + Discard rules with low accuracy. This may hurt performance a bit, + but will often produce rules which are more interesting read to a human. + """ + postag(num_sents=3000, min_acc=0.96, min_score=10) + + +def postag( + templates=None, + tagged_data=None, + num_sents=1000, + max_rules=300, + min_score=3, + min_acc=None, + train=0.8, + trace=3, + randomize=False, + ruleformat="str", + incremental_stats=False, + template_stats=False, + error_output=None, + serialize_output=None, + learning_curve_output=None, + learning_curve_take=300, + baseline_backoff_tagger=None, + separate_baseline_data=False, + cache_baseline_tagger=None, +): + """ + Brill Tagger Demonstration + :param templates: how many sentences of training and testing data to use + :type templates: list of Template + + :param tagged_data: maximum number of rule instances to create + :type tagged_data: C{int} + + :param num_sents: how many sentences of training and testing data to use + :type num_sents: C{int} + + :param max_rules: maximum number of rule instances to create + :type max_rules: C{int} + + :param min_score: the minimum score for a rule in order for it to be considered + :type min_score: C{int} + + :param min_acc: the minimum score for a rule in order for it to be considered + :type min_acc: C{float} + + :param train: the fraction of the the corpus to be used for training (1=all) + :type train: C{float} + + :param trace: the level of diagnostic tracing output to produce (0-4) + :type trace: C{int} + + :param randomize: whether the training data should be a random subset of the corpus + :type randomize: C{bool} + + :param ruleformat: rule output format, one of "str", "repr", "verbose" + :type ruleformat: C{str} + + :param incremental_stats: if true, will tag incrementally and collect stats for each rule (rather slow) + :type incremental_stats: C{bool} + + :param template_stats: if true, will print per-template statistics collected in training and (optionally) testing + :type template_stats: C{bool} + + :param error_output: the file where errors will be saved + :type error_output: C{string} + + :param serialize_output: the file where the learned tbl tagger will be saved + :type serialize_output: C{string} + + :param learning_curve_output: filename of plot of learning curve(s) (train and also test, if available) + :type learning_curve_output: C{string} + + :param learning_curve_take: how many rules plotted + :type learning_curve_take: C{int} + + :param baseline_backoff_tagger: the file where rules will be saved + :type baseline_backoff_tagger: tagger + + :param separate_baseline_data: use a fraction of the training data exclusively for training baseline + :type separate_baseline_data: C{bool} + + :param cache_baseline_tagger: cache baseline tagger to this file (only interesting as a temporary workaround to get + deterministic output from the baseline unigram tagger between python versions) + :type cache_baseline_tagger: C{string} + + + Note on separate_baseline_data: if True, reuse training data both for baseline and rule learner. This + is fast and fine for a demo, but is likely to generalize worse on unseen data. + Also cannot be sensibly used for learning curves on training data (the baseline will be artificially high). + """ + + # defaults + baseline_backoff_tagger = baseline_backoff_tagger or REGEXP_TAGGER + if templates is None: + from nltk.tag.brill import brill24, describe_template_sets + + # some pre-built template sets taken from typical systems or publications are + # available. Print a list with describe_template_sets() + # for instance: + templates = brill24() + (training_data, baseline_data, gold_data, testing_data) = _demo_prepare_data( + tagged_data, train, num_sents, randomize, separate_baseline_data + ) + + # creating (or reloading from cache) a baseline tagger (unigram tagger) + # this is just a mechanism for getting deterministic output from the baseline between + # python versions + if cache_baseline_tagger: + if not os.path.exists(cache_baseline_tagger): + baseline_tagger = UnigramTagger( + baseline_data, backoff=baseline_backoff_tagger + ) + with open(cache_baseline_tagger, "wb") as print_rules: + pickle.dump(baseline_tagger, print_rules) + print( + "Trained baseline tagger, pickled it to {}".format( + cache_baseline_tagger + ) + ) + with open(cache_baseline_tagger, "rb") as print_rules: + baseline_tagger = pickle_load(print_rules) + print(f"Reloaded pickled tagger from {cache_baseline_tagger}") + else: + baseline_tagger = UnigramTagger(baseline_data, backoff=baseline_backoff_tagger) + print("Trained baseline tagger") + if gold_data: + print( + " Accuracy on test set: {:0.4f}".format( + baseline_tagger.accuracy(gold_data) + ) + ) + + # creating a Brill tagger + tbrill = time.time() + trainer = BrillTaggerTrainer( + baseline_tagger, templates, trace, ruleformat=ruleformat + ) + print("Training tbl tagger...") + brill_tagger = trainer.train(training_data, max_rules, min_score, min_acc) + print(f"Trained tbl tagger in {time.time() - tbrill:0.2f} seconds") + if gold_data: + print(" Accuracy on test set: %.4f" % brill_tagger.accuracy(gold_data)) + + # printing the learned rules, if learned silently + if trace == 1: + print("\nLearned rules: ") + for ruleno, rule in enumerate(brill_tagger.rules(), 1): + print(f"{ruleno:4d} {rule.format(ruleformat):s}") + + # printing template statistics (optionally including comparison with the training data) + # note: if not separate_baseline_data, then baseline accuracy will be artificially high + if incremental_stats: + print( + "Incrementally tagging the test data, collecting individual rule statistics" + ) + (taggedtest, teststats) = brill_tagger.batch_tag_incremental( + testing_data, gold_data + ) + print(" Rule statistics collected") + if not separate_baseline_data: + print( + "WARNING: train_stats asked for separate_baseline_data=True; the baseline " + "will be artificially high" + ) + trainstats = brill_tagger.train_stats() + if template_stats: + brill_tagger.print_template_statistics(teststats) + if learning_curve_output: + _demo_plot( + learning_curve_output, teststats, trainstats, take=learning_curve_take + ) + print(f"Wrote plot of learning curve to {learning_curve_output}") + else: + print("Tagging the test data") + taggedtest = brill_tagger.tag_sents(testing_data) + if template_stats: + brill_tagger.print_template_statistics() + + # writing error analysis to file + if error_output is not None: + with open(error_output, "w") as f: + f.write("Errors for Brill Tagger %r\n\n" % serialize_output) + f.write("\n".join(error_list(gold_data, taggedtest)).encode("utf-8") + "\n") + print(f"Wrote tagger errors including context to {error_output}") + + # serializing the tagger to a pickle file and reloading (just to see it works) + if serialize_output is not None: + taggedtest = brill_tagger.tag_sents(testing_data) + with open(serialize_output, "wb") as print_rules: + pickle.dump(brill_tagger, print_rules) + print(f"Wrote pickled tagger to {serialize_output}") + with open(serialize_output, "rb") as print_rules: + brill_tagger_reloaded = pickle_load(print_rules) + print(f"Reloaded pickled tagger from {serialize_output}") + taggedtest_reloaded = brill_tagger_reloaded.tag_sents(testing_data) + if taggedtest == taggedtest_reloaded: + print("Reloaded tagger tried on test set, results identical") + else: + print("PROBLEM: Reloaded tagger gave different results on test set") + + +def _demo_prepare_data( + tagged_data, train, num_sents, randomize, separate_baseline_data +): + # train is the proportion of data used in training; the rest is reserved + # for testing. + if tagged_data is None: + print("Loading tagged data from treebank... ") + tagged_data = treebank.tagged_sents() + if num_sents is None or len(tagged_data) <= num_sents: + num_sents = len(tagged_data) + if randomize: + random.seed(len(tagged_data)) + random.shuffle(tagged_data) + cutoff = int(num_sents * train) + training_data = tagged_data[:cutoff] + gold_data = tagged_data[cutoff:num_sents] + testing_data = [[t[0] for t in sent] for sent in gold_data] + if not separate_baseline_data: + baseline_data = training_data + else: + bl_cutoff = len(training_data) // 3 + (baseline_data, training_data) = ( + training_data[:bl_cutoff], + training_data[bl_cutoff:], + ) + (trainseqs, traintokens) = corpus_size(training_data) + (testseqs, testtokens) = corpus_size(testing_data) + (bltrainseqs, bltraintokens) = corpus_size(baseline_data) + print(f"Read testing data ({testseqs:d} sents/{testtokens:d} wds)") + print(f"Read training data ({trainseqs:d} sents/{traintokens:d} wds)") + print( + "Read baseline data ({:d} sents/{:d} wds) {:s}".format( + bltrainseqs, + bltraintokens, + "" if separate_baseline_data else "[reused the training set]", + ) + ) + return (training_data, baseline_data, gold_data, testing_data) + + +def _demo_plot(learning_curve_output, teststats, trainstats=None, take=None): + testcurve = [teststats["initialerrors"]] + for rulescore in teststats["rulescores"]: + testcurve.append(testcurve[-1] - rulescore) + testcurve = [1 - x / teststats["tokencount"] for x in testcurve[:take]] + + traincurve = [trainstats["initialerrors"]] + for rulescore in trainstats["rulescores"]: + traincurve.append(traincurve[-1] - rulescore) + traincurve = [1 - x / trainstats["tokencount"] for x in traincurve[:take]] + + import matplotlib.pyplot as plt + + r = list(range(len(testcurve))) + plt.plot(r, testcurve, r, traincurve) + plt.axis([None, None, None, 1.0]) + plt.savefig(learning_curve_output) + + +NN_CD_TAGGER = RegexpTagger([(r"^-?[0-9]+(\.[0-9]+)?$", "CD"), (r".*", "NN")]) + +REGEXP_TAGGER = RegexpTagger( + [ + (r"^-?[0-9]+(\.[0-9]+)?$", "CD"), # cardinal numbers + (r"(The|the|A|a|An|an)$", "AT"), # articles + (r".*able$", "JJ"), # adjectives + (r".*ness$", "NN"), # nouns formed from adjectives + (r".*ly$", "RB"), # adverbs + (r".*s$", "NNS"), # plural nouns + (r".*ing$", "VBG"), # gerunds + (r".*ed$", "VBD"), # past tense verbs + (r".*", "NN"), # nouns (default) + ] +) + + +def corpus_size(seqs): + return (len(seqs), sum(len(x) for x in seqs)) + + +if __name__ == "__main__": + demo_learning_curve() diff --git a/nltk/tbl/erroranalysis.py b/nltk/tbl/erroranalysis.py new file mode 100644 index 0000000..3b43474 --- /dev/null +++ b/nltk/tbl/erroranalysis.py @@ -0,0 +1,38 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +# returns a list of errors in string format + + +def error_list(train_sents, test_sents): + """ + Returns a list of human-readable strings indicating the errors in the + given tagging of the corpus. + + :param train_sents: The correct tagging of the corpus + :type train_sents: list(tuple) + :param test_sents: The tagged corpus + :type test_sents: list(tuple) + """ + hdr = ("%25s | %s | %s\n" + "-" * 26 + "+" + "-" * 24 + "+" + "-" * 26) % ( + "left context", + "word/test->gold".center(22), + "right context", + ) + errors = [hdr] + for train_sent, test_sent in zip(train_sents, test_sents): + for wordnum, (word, train_pos) in enumerate(train_sent): + test_pos = test_sent[wordnum][1] + if train_pos != test_pos: + left = " ".join("%s/%s" % w for w in train_sent[:wordnum]) + right = " ".join("%s/%s" % w for w in train_sent[wordnum + 1 :]) + mid = f"{word}/{test_pos}->{train_pos}" + errors.append(f"{left[-25:]:>25} | {mid.center(22)} | {right[:25]}") + + return errors diff --git a/nltk/tbl/feature.py b/nltk/tbl/feature.py new file mode 100644 index 0000000..e4f4bf2 --- /dev/null +++ b/nltk/tbl/feature.py @@ -0,0 +1,267 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from abc import ABCMeta, abstractmethod + + +class Feature(metaclass=ABCMeta): + """ + An abstract base class for Features. A Feature is a combination of + a specific property-computing method and a list of relative positions + to apply that method to. + + The property-computing method, M{extract_property(tokens, index)}, + must be implemented by every subclass. It extracts or computes a specific + property for the token at the current index. Typical extract_property() + methods return features such as the token text or tag; but more involved + methods may consider the entire sequence M{tokens} and + for instance compute the length of the sentence the token belongs to. + + In addition, the subclass may have a PROPERTY_NAME, which is how + it will be printed (in Rules and Templates, etc). If not given, defaults + to the classname. + + """ + + json_tag = "nltk.tbl.Feature" + PROPERTY_NAME = None + + def __init__(self, positions, end=None): + """ + Construct a Feature which may apply at C{positions}. + + >>> # For instance, importing some concrete subclasses (Feature is abstract) + >>> from nltk.tag.brill import Word, Pos + + >>> # Feature Word, applying at one of [-2, -1] + >>> Word([-2,-1]) + Word([-2, -1]) + + >>> # Positions need not be contiguous + >>> Word([-2,-1, 1]) + Word([-2, -1, 1]) + + >>> # Contiguous ranges can alternatively be specified giving the + >>> # two endpoints (inclusive) + >>> Pos(-3, -1) + Pos([-3, -2, -1]) + + >>> # In two-arg form, start <= end is enforced + >>> Pos(2, 1) + Traceback (most recent call last): + File "", line 1, in + File "nltk/tbl/template.py", line 306, in __init__ + raise TypeError + ValueError: illegal interval specification: (start=2, end=1) + + :type positions: list of int + :param positions: the positions at which this features should apply + :raises ValueError: illegal position specifications + + An alternative calling convention, for contiguous positions only, + is Feature(start, end): + + :type start: int + :param start: start of range where this feature should apply + :type end: int + :param end: end of range (NOTE: inclusive!) where this feature should apply + """ + self.positions = None # to avoid warnings + if end is None: + self.positions = tuple(sorted({int(i) for i in positions})) + else: # positions was actually not a list, but only the start index + try: + if positions > end: + raise TypeError + self.positions = tuple(range(positions, end + 1)) + except TypeError as e: + # let any kind of erroneous spec raise ValueError + raise ValueError( + "illegal interval specification: (start={}, end={})".format( + positions, end + ) + ) from e + + # set property name given in subclass, or otherwise name of subclass + self.PROPERTY_NAME = self.__class__.PROPERTY_NAME or self.__class__.__name__ + + def encode_json_obj(self): + return self.positions + + @classmethod + def decode_json_obj(cls, obj): + positions = obj + return cls(positions) + + def __repr__(self): + return f"{self.__class__.__name__}({list(self.positions)!r})" + + @classmethod + def expand(cls, starts, winlens, excludezero=False): + """ + Return a list of features, one for each start point in starts + and for each window length in winlen. If excludezero is True, + no Features containing 0 in its positions will be generated + (many tbl trainers have a special representation for the + target feature at [0]) + + For instance, importing a concrete subclass (Feature is abstract) + + >>> from nltk.tag.brill import Word + + First argument gives the possible start positions, second the + possible window lengths + + >>> Word.expand([-3,-2,-1], [1]) + [Word([-3]), Word([-2]), Word([-1])] + + >>> Word.expand([-2,-1], [1]) + [Word([-2]), Word([-1])] + + >>> Word.expand([-3,-2,-1], [1,2]) + [Word([-3]), Word([-2]), Word([-1]), Word([-3, -2]), Word([-2, -1])] + + >>> Word.expand([-2,-1], [1]) + [Word([-2]), Word([-1])] + + A third optional argument excludes all Features whose positions contain zero + + >>> Word.expand([-2,-1,0], [1,2], excludezero=False) + [Word([-2]), Word([-1]), Word([0]), Word([-2, -1]), Word([-1, 0])] + + >>> Word.expand([-2,-1,0], [1,2], excludezero=True) + [Word([-2]), Word([-1]), Word([-2, -1])] + + All window lengths must be positive + + >>> Word.expand([-2,-1], [0]) + Traceback (most recent call last): + File "", line 1, in + File "nltk/tag/tbl/template.py", line 371, in expand + :param starts: where to start looking for Feature + ValueError: non-positive window length in [0] + + :param starts: where to start looking for Feature + :type starts: list of ints + :param winlens: window lengths where to look for Feature + :type starts: list of ints + :param excludezero: do not output any Feature with 0 in any of its positions. + :type excludezero: bool + :returns: list of Features + :raises ValueError: for non-positive window lengths + """ + if not all(x > 0 for x in winlens): + raise ValueError(f"non-positive window length in {winlens}") + xs = (starts[i : i + w] for w in winlens for i in range(len(starts) - w + 1)) + return [cls(x) for x in xs if not (excludezero and 0 in x)] + + def issuperset(self, other): + """ + Return True if this Feature always returns True when other does + + More precisely, return True if this feature refers to the same property as other; + and this Feature looks at all positions that other does (and possibly + other positions in addition). + + #For instance, importing a concrete subclass (Feature is abstract) + >>> from nltk.tag.brill import Word, Pos + + >>> Word([-3,-2,-1]).issuperset(Word([-3,-2])) + True + + >>> Word([-3,-2,-1]).issuperset(Word([-3,-2, 0])) + False + + #Feature subclasses must agree + >>> Word([-3,-2,-1]).issuperset(Pos([-3,-2])) + False + + :param other: feature with which to compare + :type other: (subclass of) Feature + :return: True if this feature is superset, otherwise False + :rtype: bool + + + """ + return self.__class__ is other.__class__ and set(self.positions) >= set( + other.positions + ) + + def intersects(self, other): + """ + Return True if the positions of this Feature intersects with those of other + + More precisely, return True if this feature refers to the same property as other; + and there is some overlap in the positions they look at. + + #For instance, importing a concrete subclass (Feature is abstract) + >>> from nltk.tag.brill import Word, Pos + + >>> Word([-3,-2,-1]).intersects(Word([-3,-2])) + True + + >>> Word([-3,-2,-1]).intersects(Word([-3,-2, 0])) + True + + >>> Word([-3,-2,-1]).intersects(Word([0])) + False + + #Feature subclasses must agree + >>> Word([-3,-2,-1]).intersects(Pos([-3,-2])) + False + + :param other: feature with which to compare + :type other: (subclass of) Feature + :return: True if feature classes agree and there is some overlap in the positions they look at + :rtype: bool + """ + + return bool( + self.__class__ is other.__class__ + and set(self.positions) & set(other.positions) + ) + + # Rich comparisons for Features. With @functools.total_ordering (Python 2.7+), + # it will be enough to define __lt__ and __eq__ + def __eq__(self, other): + return self.__class__ is other.__class__ and self.positions == other.positions + + def __lt__(self, other): + return ( + self.__class__.__name__ < other.__class__.__name__ + or + # self.positions is a sorted tuple of ints + self.positions < other.positions + ) + + def __ne__(self, other): + return not (self == other) + + def __gt__(self, other): + return other < self + + def __ge__(self, other): + return not self < other + + def __le__(self, other): + return self < other or self == other + + @staticmethod + @abstractmethod + def extract_property(tokens, index): + """ + Any subclass of Feature must define static method extract_property(tokens, index) + + :param tokens: the sequence of tokens + :type tokens: list of tokens + :param index: the current index + :type index: int + :return: feature value + :rtype: any (but usually scalar) + """ diff --git a/nltk/tbl/rule.py b/nltk/tbl/rule.py new file mode 100644 index 0000000..f917178 --- /dev/null +++ b/nltk/tbl/rule.py @@ -0,0 +1,319 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +from abc import ABCMeta, abstractmethod + +from nltk import jsontags + + +###################################################################### +# Tag Rules +###################################################################### +class TagRule(metaclass=ABCMeta): + """ + An interface for tag transformations on a tagged corpus, as + performed by tbl taggers. Each transformation finds all tokens + in the corpus that are tagged with a specific original tag and + satisfy a specific condition, and replaces their tags with a + replacement tag. For any given transformation, the original + tag, replacement tag, and condition are fixed. Conditions may + depend on the token under consideration, as well as any other + tokens in the corpus. + + Tag rules must be comparable and hashable. + """ + + def __init__(self, original_tag, replacement_tag): + self.original_tag = original_tag + """The tag which this TagRule may cause to be replaced.""" + + self.replacement_tag = replacement_tag + """The tag with which this TagRule may replace another tag.""" + + def apply(self, tokens, positions=None): + """ + Apply this rule at every position in positions where it + applies to the given sentence. I.e., for each position p + in *positions*, if *tokens[p]* is tagged with this rule's + original tag, and satisfies this rule's condition, then set + its tag to be this rule's replacement tag. + + :param tokens: The tagged sentence + :type tokens: list(tuple(str, str)) + :type positions: list(int) + :param positions: The positions where the transformation is to + be tried. If not specified, try it at all positions. + :return: The indices of tokens whose tags were changed by this + rule. + :rtype: int + """ + if positions is None: + positions = list(range(len(tokens))) + + # Determine the indices at which this rule applies. + change = [i for i in positions if self.applies(tokens, i)] + + # Make the changes. Note: this must be done in a separate + # step from finding applicable locations, since we don't want + # the rule to interact with itself. + for i in change: + tokens[i] = (tokens[i][0], self.replacement_tag) + + return change + + @abstractmethod + def applies(self, tokens, index): + """ + :return: True if the rule would change the tag of + ``tokens[index]``, False otherwise + :rtype: bool + :param tokens: A tagged sentence + :type tokens: list(str) + :param index: The index to check + :type index: int + """ + + # Rules must be comparable and hashable for the algorithm to work + def __eq__(self, other): + raise TypeError("Rules must implement __eq__()") + + def __ne__(self, other): + raise TypeError("Rules must implement __ne__()") + + def __hash__(self): + raise TypeError("Rules must implement __hash__()") + + +@jsontags.register_tag +class Rule(TagRule): + """ + A Rule checks the current corpus position for a certain set of conditions; + if they are all fulfilled, the Rule is triggered, meaning that it + will change tag A to tag B. For other tags than A, nothing happens. + + The conditions are parameters to the Rule instance. Each condition is a feature-value pair, + with a set of positions to check for the value of the corresponding feature. + Conceptually, the positions are joined by logical OR, and the feature set by logical AND. + + More formally, the Rule is then applicable to the M{n}th token iff: + + - The M{n}th token is tagged with the Rule's original tag; and + - For each (Feature(positions), M{value}) tuple: + + - The value of Feature of at least one token in {n+p for p in positions} + is M{value}. + """ + + json_tag = "nltk.tbl.Rule" + + def __init__(self, templateid, original_tag, replacement_tag, conditions): + """ + Construct a new Rule that changes a token's tag from + C{original_tag} to C{replacement_tag} if all of the properties + specified in C{conditions} hold. + + :param templateid: the template id (a zero-padded string, '001' etc, + so it will sort nicely) + :type templateid: string + + :param conditions: A list of Feature(positions), + each of which specifies that the property (computed by + Feature.extract_property()) of at least one + token in M{n} + p in positions is C{value}. + :type conditions: C{iterable} of C{Feature} + + """ + TagRule.__init__(self, original_tag, replacement_tag) + self._conditions = conditions + self.templateid = templateid + + def encode_json_obj(self): + return { + "templateid": self.templateid, + "original": self.original_tag, + "replacement": self.replacement_tag, + "conditions": self._conditions, + } + + @classmethod + def decode_json_obj(cls, obj): + return cls( + obj["templateid"], + obj["original"], + obj["replacement"], + tuple(tuple(feat) for feat in obj["conditions"]), + ) + + def applies(self, tokens, index): + # Inherit docs from TagRule + + # Does the given token have this Rule's "original tag"? + if tokens[index][1] != self.original_tag: + return False + + # Check to make sure that every condition holds. + for feature, val in self._conditions: + # Look for *any* token that satisfies the condition. + for pos in feature.positions: + if not (0 <= index + pos < len(tokens)): + continue + if feature.extract_property(tokens, index + pos) == val: + break + else: + # No token satisfied the condition; return false. + return False + + # Every condition checked out, so the Rule is applicable. + return True + + def __eq__(self, other): + return self is other or ( + other is not None + and other.__class__ == self.__class__ + and self.original_tag == other.original_tag + and self.replacement_tag == other.replacement_tag + and self._conditions == other._conditions + ) + + def __ne__(self, other): + return not (self == other) + + def __hash__(self): + # Cache our hash value (justified by profiling.) + try: + return self.__hash + except AttributeError: + self.__hash = hash(repr(self)) + return self.__hash + + def __repr__(self): + # Cache the repr (justified by profiling -- this is used as + # a sort key when deterministic=True.) + try: + return self.__repr + except AttributeError: + self.__repr = "{}('{}', {}, {}, [{}])".format( + self.__class__.__name__, + self.templateid, + repr(self.original_tag), + repr(self.replacement_tag), + # list(self._conditions) would be simpler but will not generate + # the same Rule.__repr__ in python 2 and 3 and thus break some tests + ", ".join(f"({f},{repr(v)})" for (f, v) in self._conditions), + ) + + return self.__repr + + def __str__(self): + def _condition_to_logic(feature, value): + """ + Return a compact, predicate-logic styled string representation + of the given condition. + """ + return "{}:{}@[{}]".format( + feature.PROPERTY_NAME, + value, + ",".join(str(w) for w in feature.positions), + ) + + conditions = " & ".join( + [_condition_to_logic(f, v) for (f, v) in self._conditions] + ) + s = f"{self.original_tag}->{self.replacement_tag} if {conditions}" + + return s + + def format(self, fmt): + """ + Return a string representation of this rule. + + >>> from nltk.tbl.rule import Rule + >>> from nltk.tag.brill import Pos + + >>> r = Rule("23", "VB", "NN", [(Pos([-2,-1]), 'DT')]) + + r.format("str") == str(r) + True + >>> r.format("str") + 'VB->NN if Pos:DT@[-2,-1]' + + r.format("repr") == repr(r) + True + >>> r.format("repr") + "Rule('23', 'VB', 'NN', [(Pos([-2, -1]),'DT')])" + + >>> r.format("verbose") + 'VB -> NN if the Pos of words i-2...i-1 is "DT"' + + >>> r.format("not_found") + Traceback (most recent call last): + File "", line 1, in + File "nltk/tbl/rule.py", line 256, in format + raise ValueError("unknown rule format spec: {0}".format(fmt)) + ValueError: unknown rule format spec: not_found + >>> + + :param fmt: format specification + :type fmt: str + :return: string representation + :rtype: str + """ + if fmt == "str": + return self.__str__() + elif fmt == "repr": + return self.__repr__() + elif fmt == "verbose": + return self._verbose_format() + else: + raise ValueError(f"unknown rule format spec: {fmt}") + + def _verbose_format(self): + """ + Return a wordy, human-readable string representation + of the given rule. + + Not sure how useful this is. + """ + + def condition_to_str(feature, value): + return 'the {} of {} is "{}"'.format( + feature.PROPERTY_NAME, + range_to_str(feature.positions), + value, + ) + + def range_to_str(positions): + if len(positions) == 1: + p = positions[0] + if p == 0: + return "this word" + if p == -1: + return "the preceding word" + elif p == 1: + return "the following word" + elif p < 0: + return "word i-%d" % -p + elif p > 0: + return "word i+%d" % p + else: + # for complete compatibility with the wordy format of nltk2 + mx = max(positions) + mn = min(positions) + if mx - mn == len(positions) - 1: + return "words i%+d...i%+d" % (mn, mx) + else: + return "words {{{}}}".format( + ",".join("i%+d" % d for d in positions) + ) + + replacement = f"{self.original_tag} -> {self.replacement_tag}" + conditions = (" if " if self._conditions else "") + ", and ".join( + condition_to_str(f, v) for (f, v) in self._conditions + ) + return replacement + conditions diff --git a/nltk/tbl/template.py b/nltk/tbl/template.py new file mode 100644 index 0000000..e2d7ab2 --- /dev/null +++ b/nltk/tbl/template.py @@ -0,0 +1,325 @@ +# Natural Language Toolkit: Transformation-based learning +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Marcus Uneson +# based on previous (nltk2) version by +# Christopher Maloof, Edward Loper, Steven Bird +# URL: +# For license information, see LICENSE.TXT + +import itertools as it +from abc import ABCMeta, abstractmethod + +from nltk.tbl.feature import Feature +from nltk.tbl.rule import Rule + + +class BrillTemplateI(metaclass=ABCMeta): + """ + An interface for generating lists of transformational rules that + apply at given sentence positions. ``BrillTemplateI`` is used by + ``Brill`` training algorithms to generate candidate rules. + """ + + @abstractmethod + def applicable_rules(self, tokens, i, correctTag): + """ + Return a list of the transformational rules that would correct + the ``i``-th subtoken's tag in the given token. In particular, + return a list of zero or more rules that would change + ``tokens[i][1]`` to ``correctTag``, if applied to ``token[i]``. + + If the ``i``-th token already has the correct tag (i.e., if + ``tagged_tokens[i][1] == correctTag``), then + ``applicable_rules()`` should return the empty list. + + :param tokens: The tagged tokens being tagged. + :type tokens: list(tuple) + :param i: The index of the token whose tag should be corrected. + :type i: int + :param correctTag: The correct tag for the ``i``-th token. + :type correctTag: any + :rtype: list(BrillRule) + """ + + @abstractmethod + def get_neighborhood(self, token, index): + """ + Returns the set of indices *i* such that + ``applicable_rules(token, i, ...)`` depends on the value of + the *index*th token of *token*. + + This method is used by the "fast" Brill tagger trainer. + + :param token: The tokens being tagged. + :type token: list(tuple) + :param index: The index whose neighborhood should be returned. + :type index: int + :rtype: set + """ + + +class Template(BrillTemplateI): + """ + A tbl Template that generates a list of L{Rule}s that apply at a given sentence + position. In particular, each C{Template} is parameterized by a list of + independent features (a combination of a specific + property to extract and a list C{L} of relative positions at which to extract + it) and generates all Rules that: + + - use the given features, each at its own independent position; and + - are applicable to the given token. + """ + + ALLTEMPLATES = [] + # record a unique id of form "001", for each template created + # _ids = it.count(0) + + def __init__(self, *features): + """ + Construct a Template for generating Rules. + + Takes a list of Features. A C{Feature} is a combination + of a specific property and its relative positions and should be + a subclass of L{nltk.tbl.feature.Feature}. + + An alternative calling convention (kept for backwards compatibility, + but less expressive as it only permits one feature type) is + Template(Feature, (start1, end1), (start2, end2), ...) + In new code, that would be better written + Template(Feature(start1, end1), Feature(start2, end2), ...) + + For instance, importing some features + + >>> from nltk.tbl.template import Template + >>> from nltk.tag.brill import Word, Pos + + Create some features + + >>> wfeat1, wfeat2, pfeat = (Word([-1]), Word([1,2]), Pos([-2,-1])) + + Create a single-feature template + + >>> Template(wfeat1) + Template(Word([-1])) + + Or a two-feature one + + >>> Template(wfeat1, wfeat2) + Template(Word([-1]),Word([1, 2])) + + Or a three-feature one with two different feature types + + >>> Template(wfeat1, wfeat2, pfeat) + Template(Word([-1]),Word([1, 2]),Pos([-2, -1])) + + deprecated api: Feature subclass, followed by list of (start,end) pairs + (permits only a single Feature) + + >>> Template(Word, (-2,-1), (0,0)) + Template(Word([-2, -1]),Word([0])) + + Incorrect specification raises TypeError + + >>> Template(Word, (-2,-1), Pos, (0,0)) + Traceback (most recent call last): + File "", line 1, in + File "nltk/tag/tbl/template.py", line 143, in __init__ + raise TypeError( + TypeError: expected either Feature1(args), Feature2(args), ... or Feature, (start1, end1), (start2, end2), ... + + :type features: list of Features + :param features: the features to build this Template on + """ + # determine the calling form: either + # Template(Feature, args1, [args2, ...)] + # Template(Feature1(args), Feature2(args), ...) + if all(isinstance(f, Feature) for f in features): + self._features = features + elif issubclass(features[0], Feature) and all( + isinstance(a, tuple) for a in features[1:] + ): + self._features = [features[0](*tp) for tp in features[1:]] + else: + raise TypeError( + "expected either Feature1(args), Feature2(args), ... or Feature, (start1, end1), (start2, end2), ..." + ) + self.id = f"{len(self.ALLTEMPLATES):03d}" + self.ALLTEMPLATES.append(self) + + def __repr__(self): + return "{}({})".format( + self.__class__.__name__, + ",".join([str(f) for f in self._features]), + ) + + def applicable_rules(self, tokens, index, correct_tag): + if tokens[index][1] == correct_tag: + return [] + + # For each of this Template's features, find the conditions + # that are applicable for the given token. + # Then, generate one Rule for each combination of features + # (the crossproduct of the conditions). + + applicable_conditions = self._applicable_conditions(tokens, index) + xs = list(it.product(*applicable_conditions)) + return [Rule(self.id, tokens[index][1], correct_tag, tuple(x)) for x in xs] + + def _applicable_conditions(self, tokens, index): + """ + :returns: A set of all conditions for rules + that are applicable to C{tokens[index]}. + """ + conditions = [] + + for feature in self._features: + conditions.append([]) + for pos in feature.positions: + if not (0 <= index + pos < len(tokens)): + continue + value = feature.extract_property(tokens, index + pos) + conditions[-1].append((feature, value)) + return conditions + + def get_neighborhood(self, tokens, index): + # inherit docs from BrillTemplateI + + # applicable_rules(tokens, index, ...) depends on index. + neighborhood = {index} # set literal for python 2.7+ + + # applicable_rules(tokens, i, ...) depends on index if + # i+start < index <= i+end. + + allpositions = [0] + [p for feat in self._features for p in feat.positions] + start, end = min(allpositions), max(allpositions) + s = max(0, index + (-end)) + e = min(index + (-start) + 1, len(tokens)) + for i in range(s, e): + neighborhood.add(i) + return neighborhood + + @classmethod + def expand(cls, featurelists, combinations=None, skipintersecting=True): + """ + Factory method to mass generate Templates from a list L of lists of Features. + + #With combinations=(k1, k2), the function will in all possible ways choose k1 ... k2 + #of the sublists in L; it will output all Templates formed by the Cartesian product + #of this selection, with duplicates and other semantically equivalent + #forms removed. Default for combinations is (1, len(L)). + + The feature lists may have been specified + manually, or generated from Feature.expand(). For instance, + + >>> from nltk.tbl.template import Template + >>> from nltk.tag.brill import Word, Pos + + #creating some features + >>> (wd_0, wd_01) = (Word([0]), Word([0,1])) + + >>> (pos_m2, pos_m33) = (Pos([-2]), Pos([3-2,-1,0,1,2,3])) + + >>> list(Template.expand([[wd_0], [pos_m2]])) + [Template(Word([0])), Template(Pos([-2])), Template(Pos([-2]),Word([0]))] + + >>> list(Template.expand([[wd_0, wd_01], [pos_m2]])) + [Template(Word([0])), Template(Word([0, 1])), Template(Pos([-2])), Template(Pos([-2]),Word([0])), Template(Pos([-2]),Word([0, 1]))] + + #note: with Feature.expand(), it is very easy to generate more templates + #than your system can handle -- for instance, + >>> wordtpls = Word.expand([-2,-1,0,1], [1,2], excludezero=False) + >>> len(wordtpls) + 7 + + >>> postpls = Pos.expand([-3,-2,-1,0,1,2], [1,2,3], excludezero=True) + >>> len(postpls) + 9 + + #and now the Cartesian product of all non-empty combinations of two wordtpls and + #two postpls, with semantic equivalents removed + >>> templates = list(Template.expand([wordtpls, wordtpls, postpls, postpls])) + >>> len(templates) + 713 + + + will return a list of eight templates + Template(Word([0])), + Template(Word([0, 1])), + Template(Pos([-2])), + Template(Pos([-1])), + Template(Pos([-2]),Word([0])), + Template(Pos([-1]),Word([0])), + Template(Pos([-2]),Word([0, 1])), + Template(Pos([-1]),Word([0, 1]))] + + + #Templates where one feature is a subset of another, such as + #Template(Word([0,1]), Word([1]), will not appear in the output. + #By default, this non-subset constraint is tightened to disjointness: + #Templates of type Template(Word([0,1]), Word([1,2]) will also be filtered out. + #With skipintersecting=False, then such Templates are allowed + + WARNING: this method makes it very easy to fill all your memory when training + generated templates on any real-world corpus + + :param featurelists: lists of Features, whose Cartesian product will return a set of Templates + :type featurelists: list of (list of Features) + :param combinations: given n featurelists: if combinations=k, all generated Templates will have + k features; if combinations=(k1,k2) they will have k1..k2 features; if None, defaults to 1..n + :type combinations: None, int, or (int, int) + :param skipintersecting: if True, do not output intersecting Templates (non-disjoint positions for some feature) + :type skipintersecting: bool + :returns: generator of Templates + + """ + + def nonempty_powerset(xs): # xs is a list + # itertools docnonempty_powerset([1,2,3]) --> (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3) + + # find the correct tuple given combinations, one of {None, k, (k1,k2)} + k = combinations # for brevity + combrange = ( + (1, len(xs) + 1) + if k is None + else ( + (k, k + 1) # n over 1 .. n over n (all non-empty combinations) + if isinstance(k, int) + else (k[0], k[1] + 1) + ) # n over k (only + ) # n over k1, n over k1+1... n over k2 + return it.chain.from_iterable( + it.combinations(xs, r) for r in range(*combrange) + ) + + seentemplates = set() + for picks in nonempty_powerset(featurelists): + for pick in it.product(*picks): + if any( + i != j and x.issuperset(y) + for (i, x) in enumerate(pick) + for (j, y) in enumerate(pick) + ): + continue + if skipintersecting and any( + i != j and x.intersects(y) + for (i, x) in enumerate(pick) + for (j, y) in enumerate(pick) + ): + continue + thistemplate = cls(*sorted(pick)) + strpick = str(thistemplate) + #!!FIXME --this is hackish + if strpick in seentemplates: # already added + cls._poptemplate() + continue + seentemplates.add(strpick) + yield thistemplate + + @classmethod + def _cleartemplates(cls): + cls.ALLTEMPLATES = [] + + @classmethod + def _poptemplate(cls): + return cls.ALLTEMPLATES.pop() if cls.ALLTEMPLATES else None diff --git a/nltk/test/FX8.xml b/nltk/test/FX8.xml new file mode 100644 index 0000000..58631da --- /dev/null +++ b/nltk/test/FX8.xml @@ -0,0 +1,17 @@ + General practitioner's surgery: medical consultation. Sample containing about 125 words speech recorded in public context Data capture and transcription Longman ELT BNC XML Edition, December 2006 125 tokens; 130 w-units; 15 s-units Distributed under licence by Oxford University Computing Services on behalf of the BNC Consortium. This material is protected by international copyright laws and may not be copied or redistributed in any way. Consult the BNC Web Site at http://www.natcorp.ox.ac.uk for full licencing and distribution conditions.FX8 093802 0000-00-00 Origination/creation date not known Doctor doctor other participants are doctors patientsUnknown speakerGroup of unknown speakersStrathclyde: Lanarkshire G.P.'s surgery Medical consultation S consult medicine medical consultation Tag usage updated for BNC-XMLLast check for BNC World first releaseCheck all tagcountsResequenced s-units and added headersRevised participant detailsAdded date infoUpdated all catrefsUpdated REC elements to include tape numberUpdated titlescorrected tagUsagePOS codes revised for BNC-2; header updatedInitial accession to corpus + +Ah there we are,. +Right abdominal wound, she's a wee bit confused. +She didn't bother to tell me that she'd only got to call you, right? +Erm she wasn't in her nightdress but she only dressed herself, she said +And you +She said she went to buy something herself, she phoned the clinic and the clinic . +She's here and says she should be fortnightly . +So I don't know whether you want to go and see her rather than, I could get a doctor to go and see her and phone,. +it's just that I'm never gonna get to up to. +? +Yeah. +Okay. +Yeah. +erm, first twelve weeks pregnant so should I mark at the bottom when she types . +Erm this one. diff --git a/nltk/test/Makefile b/nltk/test/Makefile new file mode 100644 index 0000000..984182e --- /dev/null +++ b/nltk/test/Makefile @@ -0,0 +1,28 @@ +.SUFFIXES: .doctest .errs .html + +TESTS = $(wildcard *.doctest) + +ERRS := $(TESTS:.doctest=.errs) + +HTML = $(TESTS:.doctest=.html) # $(IPYNB:.ipynb=.html) + +IPYNB = $(wildcard *.ipynb) + +.doctest.errs: + pytest $< > $@ + +.doctest.html: + rst2html.py $< > $@ + +%.html: %.ipynb + ipython nbconvert $< + +all: $(ERRS) + +html: $(HTML) + +install_html: + cp $(HTML) ../../../nltk.github.com/howto + +clean: + rm -f *.errs diff --git a/nltk/test/__init__.py b/nltk/test/__init__.py new file mode 100644 index 0000000..6554f14 --- /dev/null +++ b/nltk/test/__init__.py @@ -0,0 +1,18 @@ +# Natural Language Toolkit: Unit Tests +# +# Copyright (C) 2001-2026 NLTK Project +# Author: Edward Loper +# URL: +# For license information, see LICENSE.TXT + +""" +Unit tests for the NLTK modules. These tests are intended to ensure +that source code changes don't accidentally introduce bugs. +For instructions, please see: + +../../web/dev/local_testing.rst + +https://github.com/nltk/nltk/blob/develop/web/dev/local_testing.rst + + +""" diff --git a/nltk/test/all.py b/nltk/test/all.py new file mode 100644 index 0000000..5028409 --- /dev/null +++ b/nltk/test/all.py @@ -0,0 +1,26 @@ +"""Test suite that runs all NLTK tests. + +This module, `nltk.test.all`, is named as the NLTK ``test_suite`` in the +project's ``setup-eggs.py`` file. Here, we create a test suite that +runs all of our doctests, and return it for processing by the setuptools +test harness. + +""" + +import doctest +import os.path +import unittest +from glob import glob + + +def additional_tests(): + # print("here-000000000000000") + # print("-----", glob(os.path.join(os.path.dirname(__file__), '*.doctest'))) + dir = os.path.dirname(__file__) + paths = glob(os.path.join(dir, "*.doctest")) + files = [os.path.basename(path) for path in paths] + return unittest.TestSuite([doctest.DocFileSuite(file) for file in files]) + + +# if os.path.split(path)[-1] != 'index.rst' +# skips time-dependent doctest in index.rst diff --git a/nltk/test/bleu.doctest b/nltk/test/bleu.doctest new file mode 100644 index 0000000..4ea6b8a --- /dev/null +++ b/nltk/test/bleu.doctest @@ -0,0 +1,29 @@ +========== +BLEU tests +========== + +>>> from nltk.translate import bleu + +If the candidate has no alignment to any of the references, the BLEU score is 0. + +>>> bleu( +... ['The candidate has no alignment to any of the references'.split()], +... 'John loves Mary'.split(), +... (1,), +... ) +0 + +This is an implementation of the smoothing techniques +for segment-level BLEU scores that was presented in +Boxing Chen and Collin Cherry (2014) A Systematic Comparison of +Smoothing Techniques for Sentence-Level BLEU. In WMT14. +http://acl2014.org/acl2014/W14-33/pdf/W14-3346.pdf +>>> from nltk.translate.bleu_score import sentence_bleu,SmoothingFunction + + +>>> sentence_bleu( +... ['It is a place of quiet contemplation .'.split()], +... 'It is .'.split(), +... smoothing_function=SmoothingFunction().method4, +... )*100 +4.4267... diff --git a/nltk/test/bnc.doctest b/nltk/test/bnc.doctest new file mode 100644 index 0000000..699b694 --- /dev/null +++ b/nltk/test/bnc.doctest @@ -0,0 +1,60 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + + >>> import os.path + + >>> from nltk.corpus.reader import BNCCorpusReader + >>> import nltk.test + + >>> root = os.path.dirname(nltk.test.__file__) + >>> bnc = BNCCorpusReader(root=root, fileids='FX8.xml') + +Checking the word access. +------------------------- + + >>> len(bnc.words()) + 151 + + >>> bnc.words()[:6] + ['Ah', 'there', 'we', 'are', ',', '.'] + >>> bnc.words(stem=True)[:6] + ['ah', 'there', 'we', 'be', ',', '.'] + + >>> bnc.tagged_words()[:6] + [('Ah', 'INTERJ'), ('there', 'ADV'), ('we', 'PRON'), ('are', 'VERB'), (',', 'PUN'), ('.', 'PUN')] + + >>> bnc.tagged_words(c5=True)[:6] + [('Ah', 'ITJ'), ('there', 'AV0'), ('we', 'PNP'), ('are', 'VBB'), (',', 'PUN'), ('.', 'PUN')] + +Testing access to the sentences. +-------------------------------- + + >>> len(bnc.sents()) + 15 + + >>> bnc.sents()[0] + ['Ah', 'there', 'we', 'are', ',', '.'] + >>> bnc.sents(stem=True)[0] + ['ah', 'there', 'we', 'be', ',', '.'] + + >>> bnc.tagged_sents()[0] + [('Ah', 'INTERJ'), ('there', 'ADV'), ('we', 'PRON'), ('are', 'VERB'), (',', 'PUN'), ('.', 'PUN')] + >>> bnc.tagged_sents(c5=True)[0] + [('Ah', 'ITJ'), ('there', 'AV0'), ('we', 'PNP'), ('are', 'VBB'), (',', 'PUN'), ('.', 'PUN')] + +A not lazy loader. +------------------ + + >>> eager = BNCCorpusReader(root=root, fileids=r'FX8.xml', lazy=False) + + >>> len(eager.words()) + 151 + >>> eager.words(stem=True)[6:17] + ['right', 'abdominal', 'wound', ',', 'she', 'be', 'a', 'wee', 'bit', 'confuse', '.'] + + >>> eager.tagged_words()[6:11] + [('Right', 'ADV'), ('abdominal', 'ADJ'), ('wound', 'SUBST'), (',', 'PUN'), ('she', 'PRON')] + >>> eager.tagged_words(c5=True)[6:17] + [('Right', 'AV0'), ('abdominal', 'AJ0'), ('wound', 'NN1'), (',', 'PUN'), ('she', 'PNP'), ("'s", 'VBZ'), ('a', 'AT0'), ('wee', 'AJ0-NN1'), ('bit', 'NN1'), ('confused', 'VVN-AJ0'), ('.', 'PUN')] + >>> len(eager.sents()) + 15 diff --git a/nltk/test/ccg.doctest b/nltk/test/ccg.doctest new file mode 100644 index 0000000..5a74659 --- /dev/null +++ b/nltk/test/ccg.doctest @@ -0,0 +1,376 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +============================== +Combinatory Categorial Grammar +============================== + +Relative Clauses +---------------- + + >>> from nltk.ccg import chart, lexicon + +Construct a lexicon: + + >>> lex = lexicon.fromstring(''' + ... :- S, NP, N, VP + ... + ... Det :: NP/N + ... Pro :: NP + ... Modal :: S\\NP/VP + ... + ... TV :: VP/NP + ... DTV :: TV/NP + ... + ... the => Det + ... + ... that => Det + ... that => NP + ... + ... I => Pro + ... you => Pro + ... we => Pro + ... + ... chef => N + ... cake => N + ... children => N + ... dough => N + ... + ... will => Modal + ... should => Modal + ... might => Modal + ... must => Modal + ... + ... and => var\\.,var/.,var + ... + ... to => VP[to]/VP + ... + ... without => (VP\\VP)/VP[ing] + ... + ... be => TV + ... cook => TV + ... eat => TV + ... + ... cooking => VP[ing]/NP + ... + ... give => DTV + ... + ... is => (S\\NP)/NP + ... prefer => (S\\NP)/NP + ... + ... which => (N\\N)/(S/NP) + ... + ... persuade => (VP/VP[to])/NP + ... ''') + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> for parse in parser.parse("you prefer that cake".split()): + ... chart.printCCGDerivation(parse) + ... break + ... + you prefer that cake + NP ((S\NP)/NP) (NP/N) N + --------------> + NP + ---------------------------> + (S\NP) + --------------------------------< + S + + >>> for parse in parser.parse("that is the cake which you prefer".split()): + ... chart.printCCGDerivation(parse) + ... break + ... + that is the cake which you prefer + NP ((S\NP)/NP) (NP/N) N ((N\N)/(S/NP)) NP ((S\NP)/NP) + ----->T + (S/(S\NP)) + ------------------>B + (S/NP) + ----------------------------------> + (N\N) + ----------------------------------------< + N + ------------------------------------------------> + NP + -------------------------------------------------------------> + (S\NP) + -------------------------------------------------------------------< + S + + +Some other sentences to try: +"that is the cake which we will persuade the chef to cook" +"that is the cake which we will persuade the chef to give the children" + + >>> sent = "that is the dough which you will eat without cooking".split() + >>> nosub_parser = chart.CCGChartParser(lex, chart.ApplicationRuleSet + + ... chart.CompositionRuleSet + chart.TypeRaiseRuleSet) + +Without Substitution (no output) + + >>> for parse in nosub_parser.parse(sent): + ... chart.printCCGDerivation(parse) + +With Substitution: + + >>> for parse in parser.parse(sent): + ... chart.printCCGDerivation(parse) + ... break + ... + that is the dough which you will eat without cooking + NP ((S\NP)/NP) (NP/N) N ((N\N)/(S/NP)) NP ((S\NP)/VP) (VP/NP) ((VP\VP)/VP['ing']) (VP['ing']/NP) + ----->T + (S/(S\NP)) + ------------------------------------->B + ((VP\VP)/NP) + ----------------------------------------------B + ((S\NP)/NP) + ---------------------------------------------------------------->B + (S/NP) + --------------------------------------------------------------------------------> + (N\N) + ---------------------------------------------------------------------------------------< + N + -----------------------------------------------------------------------------------------------> + NP + ------------------------------------------------------------------------------------------------------------> + (S\NP) + ------------------------------------------------------------------------------------------------------------------< + S + + +Conjunction +----------- + + >>> from nltk.ccg.chart import CCGChartParser, ApplicationRuleSet, CompositionRuleSet + >>> from nltk.ccg.chart import SubstitutionRuleSet, TypeRaiseRuleSet, printCCGDerivation + >>> from nltk.ccg import lexicon + +Lexicons for the tests: + + >>> test1_lex = ''' + ... :- S,N,NP,VP + ... I => NP + ... you => NP + ... will => S\\NP/VP + ... cook => VP/NP + ... which => (N\\N)/(S/NP) + ... and => var\\.,var/.,var + ... might => S\\NP/VP + ... eat => VP/NP + ... the => NP/N + ... mushrooms => N + ... parsnips => N''' + >>> test2_lex = ''' + ... :- N, S, NP, VP + ... articles => N + ... the => NP/N + ... and => var\\.,var/.,var + ... which => (N\\N)/(S/NP) + ... I => NP + ... anyone => NP + ... will => (S/VP)\\NP + ... file => VP/NP + ... without => (VP\\VP)/VP[ing] + ... forget => VP/NP + ... reading => VP[ing]/NP + ... ''' + +Tests handling of conjunctions. +Note that while the two derivations are different, they are semantically equivalent. + + >>> lex = lexicon.fromstring(test1_lex) + >>> parser = CCGChartParser(lex, ApplicationRuleSet + CompositionRuleSet + SubstitutionRuleSet) + >>> for parse in parser.parse("I will cook and might eat the mushrooms and parsnips".split()): + ... printCCGDerivation(parse) + I will cook and might eat the mushrooms and parsnips + NP ((S\NP)/VP) (VP/NP) ((_var0\.,_var0)/.,_var0) ((S\NP)/VP) (VP/NP) (NP/N) N ((_var0\.,_var0)/.,_var0) N + ---------------------->B + ((S\NP)/NP) + ---------------------->B + ((S\NP)/NP) + -------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) + -----------------------------------------------------------------------< + ((S\NP)/NP) + -------------------------------------> + (N\.,N) + ------------------------------------------------< + N + --------------------------------------------------------> + NP + -------------------------------------------------------------------------------------------------------------------------------> + (S\NP) + -----------------------------------------------------------------------------------------------------------------------------------< + S + I will cook and might eat the mushrooms and parsnips + NP ((S\NP)/VP) (VP/NP) ((_var0\.,_var0)/.,_var0) ((S\NP)/VP) (VP/NP) (NP/N) N ((_var0\.,_var0)/.,_var0) N + ---------------------->B + ((S\NP)/NP) + ---------------------->B + ((S\NP)/NP) + -------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) + -----------------------------------------------------------------------< + ((S\NP)/NP) + ------------------------------------------------------------------------------->B + ((S\NP)/N) + -------------------------------------> + (N\.,N) + ------------------------------------------------< + N + -------------------------------------------------------------------------------------------------------------------------------> + (S\NP) + -----------------------------------------------------------------------------------------------------------------------------------< + S + + +Tests handling subject extraction. +Interesting to point that the two parses are clearly semantically different. + + >>> lex = lexicon.fromstring(test2_lex) + >>> parser = CCGChartParser(lex, ApplicationRuleSet + CompositionRuleSet + SubstitutionRuleSet) + >>> for parse in parser.parse("articles which I will file and forget without reading".split()): + ... printCCGDerivation(parse) + articles which I will file and forget without reading + N ((N\N)/(S/NP)) NP ((S/VP)\NP) (VP/NP) ((_var0\.,_var0)/.,_var0) (VP/NP) ((VP\VP)/VP['ing']) (VP['ing']/NP) + -----------------< + (S/VP) + ------------------------------------->B + ((VP\VP)/NP) + ---------------------------------------------- + ((VP/NP)\.,(VP/NP)) + ----------------------------------------------------------------------------------< + (VP/NP) + --------------------------------------------------------------------------------------------------->B + (S/NP) + -------------------------------------------------------------------------------------------------------------------> + (N\N) + -----------------------------------------------------------------------------------------------------------------------------< + N + articles which I will file and forget without reading + N ((N\N)/(S/NP)) NP ((S/VP)\NP) (VP/NP) ((_var0\.,_var0)/.,_var0) (VP/NP) ((VP\VP)/VP['ing']) (VP['ing']/NP) + -----------------< + (S/VP) + ------------------------------------> + ((VP/NP)\.,(VP/NP)) + ---------------------------------------------< + (VP/NP) + ------------------------------------->B + ((VP\VP)/NP) + ----------------------------------------------------------------------------------B + (S/NP) + -------------------------------------------------------------------------------------------------------------------> + (N\N) + -----------------------------------------------------------------------------------------------------------------------------< + N + + +Unicode support +--------------- + +Unicode words are supported. + + >>> from nltk.ccg import chart, lexicon + +Lexicons for the tests: + + >>> lex = lexicon.fromstring(''' + ... :- S, N, NP, PP + ... + ... AdjI :: N\\N + ... AdjD :: N/N + ... AdvD :: S/S + ... AdvI :: S\\S + ... Det :: NP/N + ... PrepNPCompl :: PP/NP + ... PrepNAdjN :: S\\S/N + ... PrepNAdjNP :: S\\S/NP + ... VPNP :: S\\NP/NP + ... VPPP :: S\\NP/PP + ... VPser :: S\\NP/AdjI + ... + ... auto => N + ... bebidas => N + ... cine => N + ... ley => N + ... libro => N + ... ministro => N + ... panadería => N + ... presidente => N + ... super => N + ... + ... el => Det + ... la => Det + ... las => Det + ... un => Det + ... + ... Ana => NP + ... Pablo => NP + ... + ... y => var\\.,var/.,var + ... + ... pero => (S/NP)\\(S/NP)/(S/NP) + ... + ... anunció => VPNP + ... compró => VPNP + ... cree => S\\NP/S[dep] + ... desmintió => VPNP + ... lee => VPNP + ... fueron => VPPP + ... + ... es => VPser + ... + ... interesante => AdjD + ... interesante => AdjI + ... nueva => AdjD + ... nueva => AdjI + ... + ... a => PrepNPCompl + ... en => PrepNAdjN + ... en => PrepNAdjNP + ... + ... ayer => AdvI + ... + ... que => (NP\\NP)/(S/NP) + ... que => S[dep]/S + ... ''') + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> for parse in parser.parse(u"el ministro anunció pero el presidente desmintió la nueva ley".split()): + ... printCCGDerivation(parse) # doctest: +SKIP + ... # it fails on python2.7 because of the unicode problem explained in https://github.com/nltk/nltk/pull/1354 + ... break + el ministro anunció pero el presidente desmintió la nueva ley + (NP/N) N ((S\NP)/NP) (((S/NP)\(S/NP))/(S/NP)) (NP/N) N ((S\NP)/NP) (NP/N) (N/N) N + ------------------> + NP + ------------------>T + (S/(S\NP)) + --------------------> + NP + -------------------->T + (S/(S\NP)) + --------------------------------->B + (S/NP) + -----------------------------------------------------------> + ((S/NP)\(S/NP)) + ------------> + N + --------------------> + NP + -------------------- + S diff --git a/nltk/test/ccg_semantics.doctest b/nltk/test/ccg_semantics.doctest new file mode 100644 index 0000000..af0fdd0 --- /dev/null +++ b/nltk/test/ccg_semantics.doctest @@ -0,0 +1,552 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +============================================== +Combinatory Categorial Grammar with semantics +============================================== + +----- +Chart +----- + + + >>> from nltk.ccg import chart, lexicon + >>> from nltk.ccg.chart import printCCGDerivation + +No semantics +------------------- + + >>> lex = lexicon.fromstring(''' + ... :- S, NP, N + ... She => NP + ... has => (S\\NP)/NP + ... books => NP + ... ''', + ... False) + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> parses = list(parser.parse("She has books".split())) + >>> print(str(len(parses)) + " parses") + 3 parses + + >>> printCCGDerivation(parses[0]) + She has books + NP ((S\NP)/NP) NP + --------------------> + (S\NP) + -------------------------< + S + + >>> printCCGDerivation(parses[1]) + She has books + NP ((S\NP)/NP) NP + ----->T + (S/(S\NP)) + --------------------> + (S\NP) + -------------------------> + S + + + >>> printCCGDerivation(parses[2]) + She has books + NP ((S\NP)/NP) NP + ----->T + (S/(S\NP)) + ------------------>B + (S/NP) + -------------------------> + S + +Simple semantics +------------------- + + >>> lex = lexicon.fromstring(''' + ... :- S, NP, N + ... She => NP {she} + ... has => (S\\NP)/NP {\\x y.have(y, x)} + ... a => NP/N {\\P.exists z.P(z)} + ... book => N {book} + ... ''', + ... True) + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> parses = list(parser.parse("She has a book".split())) + >>> print(str(len(parses)) + " parses") + 7 parses + + >>> printCCGDerivation(parses[0]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + -------------------------------------> + NP {exists x.book(x)} + -------------------------------------------------------------------> + (S\NP) {\x.have(x,exists y.book(y))} + -----------------------------------------------------------------------------< + S {have(she,exists x.book(x))} + + >>> printCCGDerivation(parses[1]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + --------------------------------------------------------->B + ((S\NP)/N) {\F x.have(x,exists y.F(y))} + -------------------------------------------------------------------> + (S\NP) {\x.have(x,exists y.book(y))} + -----------------------------------------------------------------------------< + S {have(she,exists x.book(x))} + + >>> printCCGDerivation(parses[2]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + ---------->T + (S/(S\NP)) {\F.F(she)} + -------------------------------------> + NP {exists x.book(x)} + -------------------------------------------------------------------> + (S\NP) {\x.have(x,exists y.book(y))} + -----------------------------------------------------------------------------> + S {have(she,exists x.book(x))} + + >>> printCCGDerivation(parses[3]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + ---------->T + (S/(S\NP)) {\F.F(she)} + --------------------------------------------------------->B + ((S\NP)/N) {\F x.have(x,exists y.F(y))} + -------------------------------------------------------------------> + (S\NP) {\x.have(x,exists y.book(y))} + -----------------------------------------------------------------------------> + S {have(she,exists x.book(x))} + + >>> printCCGDerivation(parses[4]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + ---------->T + (S/(S\NP)) {\F.F(she)} + ---------------------------------------->B + (S/NP) {\x.have(she,x)} + -------------------------------------> + NP {exists x.book(x)} + -----------------------------------------------------------------------------> + S {have(she,exists x.book(x))} + + >>> printCCGDerivation(parses[5]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + ---------->T + (S/(S\NP)) {\F.F(she)} + --------------------------------------------------------->B + ((S\NP)/N) {\F x.have(x,exists y.F(y))} + ------------------------------------------------------------------->B + (S/N) {\F.have(she,exists x.F(x))} + -----------------------------------------------------------------------------> + S {have(she,exists x.book(x))} + + >>> printCCGDerivation(parses[6]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (NP/N) {\P.exists z.P(z)} N {book} + ---------->T + (S/(S\NP)) {\F.F(she)} + ---------------------------------------->B + (S/NP) {\x.have(she,x)} + ------------------------------------------------------------------->B + (S/N) {\F.have(she,exists x.F(x))} + -----------------------------------------------------------------------------> + S {have(she,exists x.book(x))} + +Complex semantics +------------------- + + >>> lex = lexicon.fromstring(''' + ... :- S, NP, N + ... She => NP {she} + ... has => (S\\NP)/NP {\\x y.have(y, x)} + ... a => ((S\\NP)\\((S\\NP)/NP))/N {\\P R x.(exists z.P(z) & R(z,x))} + ... book => N {book} + ... ''', + ... True) + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> parses = list(parser.parse("She has a book".split())) + >>> print(str(len(parses)) + " parses") + 2 parses + + >>> printCCGDerivation(parses[0]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (((S\NP)\((S\NP)/NP))/N) {\P R x.(exists z.P(z) & R(z,x))} N {book} + ----------------------------------------------------------------------> + ((S\NP)\((S\NP)/NP)) {\P x.(exists y.book(y) & P(z,x))} + ----------------------------------------------------------------------------------------------------< + (S\NP) {\x.(exists y.book(y) & have(x,z))} + --------------------------------------------------------------------------------------------------------------< + S {(exists x.book(x) & have(she,z))} + + >>> printCCGDerivation(parses[1]) + She has a book + NP {she} ((S\NP)/NP) {\x y.have(y,x)} (((S\NP)\((S\NP)/NP))/N) {\P R x.(exists z.P(z) & R(z,x))} N {book} + ---------->T + (S/(S\NP)) {\F.F(she)} + ----------------------------------------------------------------------> + ((S\NP)\((S\NP)/NP)) {\P x.(exists y.book(y) & P(z,x))} + ----------------------------------------------------------------------------------------------------< + (S\NP) {\x.(exists y.book(y) & have(x,z))} + --------------------------------------------------------------------------------------------------------------> + S {(exists x.book(x) & have(she,z))} + +Using conjunctions +--------------------- + + # TODO: The semantics of "and" should have been more flexible + >>> lex = lexicon.fromstring(''' + ... :- S, NP, N + ... I => NP {I} + ... cook => (S\\NP)/NP {\\x y.cook(x,y)} + ... and => var\\.,var/.,var {\\P Q x y.(P(x,y) & Q(x,y))} + ... eat => (S\\NP)/NP {\\x y.eat(x,y)} + ... the => NP/N {\\x.the(x)} + ... bacon => N {bacon} + ... ''', + ... True) + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> parses = list(parser.parse("I cook and eat the bacon".split())) + >>> print(str(len(parses)) + " parses") + 7 parses + + >>> printCCGDerivation(parses[0]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + -------------------------------> + NP {the(bacon)} + --------------------------------------------------------------------------------------------------------------------------------------------------> + (S\NP) {\x.(eat(the(bacon),x) & cook(the(bacon),x))} + ----------------------------------------------------------------------------------------------------------------------------------------------------------< + S {(eat(the(bacon),I) & cook(the(bacon),I))} + + >>> printCCGDerivation(parses[1]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + --------------------------------------------------------------------------------------------------------------------------------------->B + ((S\NP)/N) {\x y.(eat(the(x),y) & cook(the(x),y))} + --------------------------------------------------------------------------------------------------------------------------------------------------> + (S\NP) {\x.(eat(the(bacon),x) & cook(the(bacon),x))} + ----------------------------------------------------------------------------------------------------------------------------------------------------------< + S {(eat(the(bacon),I) & cook(the(bacon),I))} + + >>> printCCGDerivation(parses[2]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------->T + (S/(S\NP)) {\F.F(I)} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + -------------------------------> + NP {the(bacon)} + --------------------------------------------------------------------------------------------------------------------------------------------------> + (S\NP) {\x.(eat(the(bacon),x) & cook(the(bacon),x))} + ----------------------------------------------------------------------------------------------------------------------------------------------------------> + S {(eat(the(bacon),I) & cook(the(bacon),I))} + + >>> printCCGDerivation(parses[3]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------->T + (S/(S\NP)) {\F.F(I)} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + --------------------------------------------------------------------------------------------------------------------------------------->B + ((S\NP)/N) {\x y.(eat(the(x),y) & cook(the(x),y))} + --------------------------------------------------------------------------------------------------------------------------------------------------> + (S\NP) {\x.(eat(the(bacon),x) & cook(the(bacon),x))} + ----------------------------------------------------------------------------------------------------------------------------------------------------------> + S {(eat(the(bacon),I) & cook(the(bacon),I))} + + >>> printCCGDerivation(parses[4]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------->T + (S/(S\NP)) {\F.F(I)} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + --------------------------------------------------------------------------------------------------------------------------->B + (S/NP) {\x.(eat(x,I) & cook(x,I))} + -------------------------------> + NP {the(bacon)} + ----------------------------------------------------------------------------------------------------------------------------------------------------------> + S {(eat(the(bacon),I) & cook(the(bacon),I))} + + >>> printCCGDerivation(parses[5]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------->T + (S/(S\NP)) {\F.F(I)} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + --------------------------------------------------------------------------------------------------------------------------------------->B + ((S\NP)/N) {\x y.(eat(the(x),y) & cook(the(x),y))} + ----------------------------------------------------------------------------------------------------------------------------------------------->B + (S/N) {\x.(eat(the(x),I) & cook(the(x),I))} + ----------------------------------------------------------------------------------------------------------------------------------------------------------> + S {(eat(the(bacon),I) & cook(the(bacon),I))} + + >>> printCCGDerivation(parses[6]) + I cook and eat the bacon + NP {I} ((S\NP)/NP) {\x y.cook(x,y)} ((_var0\.,_var0)/.,_var0) {\P Q x y.(P(x,y) & Q(x,y))} ((S\NP)/NP) {\x y.eat(x,y)} (NP/N) {\x.the(x)} N {bacon} + -------->T + (S/(S\NP)) {\F.F(I)} + -------------------------------------------------------------------------------------> + (((S\NP)/NP)\.,((S\NP)/NP)) {\P x y.(eat(x,y) & P(x,y))} + -------------------------------------------------------------------------------------------------------------------< + ((S\NP)/NP) {\x y.(eat(x,y) & cook(x,y))} + --------------------------------------------------------------------------------------------------------------------------->B + (S/NP) {\x.(eat(x,I) & cook(x,I))} + ----------------------------------------------------------------------------------------------------------------------------------------------->B + (S/N) {\x.(eat(the(x),I) & cook(the(x),I))} + ----------------------------------------------------------------------------------------------------------------------------------------------------------> + S {(eat(the(bacon),I) & cook(the(bacon),I))} + +Tests from published papers +------------------------------ + +An example from "CCGbank: A Corpus of CCG Derivations and Dependency Structures Extracted from the Penn Treebank", Hockenmaier and Steedman, 2007, Page 359, https://www.aclweb.org/anthology/J/J07/J07-3004.pdf + + >>> lex = lexicon.fromstring(''' + ... :- S, NP + ... I => NP {I} + ... give => ((S\\NP)/NP)/NP {\\x y z.give(y,x,z)} + ... them => NP {them} + ... money => NP {money} + ... ''', + ... True) + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> parses = list(parser.parse("I give them money".split())) + >>> print(str(len(parses)) + " parses") + 3 parses + + >>> printCCGDerivation(parses[0]) + I give them money + NP {I} (((S\NP)/NP)/NP) {\x y z.give(y,x,z)} NP {them} NP {money} + --------------------------------------------------> + ((S\NP)/NP) {\x y.give(x,them,y)} + --------------------------------------------------------------> + (S\NP) {\x.give(money,them,x)} + ----------------------------------------------------------------------< + S {give(money,them,I)} + + >>> printCCGDerivation(parses[1]) + I give them money + NP {I} (((S\NP)/NP)/NP) {\x y z.give(y,x,z)} NP {them} NP {money} + -------->T + (S/(S\NP)) {\F.F(I)} + --------------------------------------------------> + ((S\NP)/NP) {\x y.give(x,them,y)} + --------------------------------------------------------------> + (S\NP) {\x.give(money,them,x)} + ----------------------------------------------------------------------> + S {give(money,them,I)} + + + >>> printCCGDerivation(parses[2]) + I give them money + NP {I} (((S\NP)/NP)/NP) {\x y z.give(y,x,z)} NP {them} NP {money} + -------->T + (S/(S\NP)) {\F.F(I)} + --------------------------------------------------> + ((S\NP)/NP) {\x y.give(x,them,y)} + ---------------------------------------------------------->B + (S/NP) {\x.give(x,them,I)} + ----------------------------------------------------------------------> + S {give(money,them,I)} + + +An example from "CCGbank: A Corpus of CCG Derivations and Dependency Structures Extracted from the Penn Treebank", Hockenmaier and Steedman, 2007, Page 359, https://www.aclweb.org/anthology/J/J07/J07-3004.pdf + + >>> lex = lexicon.fromstring(''' + ... :- N, NP, S + ... money => N {money} + ... that => (N\\N)/(S/NP) {\\P Q x.(P(x) & Q(x))} + ... I => NP {I} + ... give => ((S\\NP)/NP)/NP {\\x y z.give(y,x,z)} + ... them => NP {them} + ... ''', + ... True) + + >>> parser = chart.CCGChartParser(lex, chart.DefaultRuleSet) + >>> parses = list(parser.parse("money that I give them".split())) + >>> print(str(len(parses)) + " parses") + 3 parses + + >>> printCCGDerivation(parses[0]) + money that I give them + N {money} ((N\N)/(S/NP)) {\P Q x.(P(x) & Q(x))} NP {I} (((S\NP)/NP)/NP) {\x y z.give(y,x,z)} NP {them} + -------->T + (S/(S\NP)) {\F.F(I)} + --------------------------------------------------> + ((S\NP)/NP) {\x y.give(x,them,y)} + ---------------------------------------------------------->B + (S/NP) {\x.give(x,them,I)} + -------------------------------------------------------------------------------------------------> + (N\N) {\P x.(give(x,them,I) & P(x))} + ------------------------------------------------------------------------------------------------------------< + N {\x.(give(x,them,I) & money(x))} + + >>> printCCGDerivation(parses[1]) + money that I give them + N {money} ((N\N)/(S/NP)) {\P Q x.(P(x) & Q(x))} NP {I} (((S\NP)/NP)/NP) {\x y z.give(y,x,z)} NP {them} + ----------->T + (N/(N\N)) {\F.F(money)} + -------->T + (S/(S\NP)) {\F.F(I)} + --------------------------------------------------> + ((S\NP)/NP) {\x y.give(x,them,y)} + ---------------------------------------------------------->B + (S/NP) {\x.give(x,them,I)} + -------------------------------------------------------------------------------------------------> + (N\N) {\P x.(give(x,them,I) & P(x))} + ------------------------------------------------------------------------------------------------------------> + N {\x.(give(x,them,I) & money(x))} + + >>> printCCGDerivation(parses[2]) + money that I give them + N {money} ((N\N)/(S/NP)) {\P Q x.(P(x) & Q(x))} NP {I} (((S\NP)/NP)/NP) {\x y z.give(y,x,z)} NP {them} + ----------->T + (N/(N\N)) {\F.F(money)} + -------------------------------------------------->B + (N/(S/NP)) {\F x.(F(x) & money(x))} + -------->T + (S/(S\NP)) {\F.F(I)} + --------------------------------------------------> + ((S\NP)/NP) {\x y.give(x,them,y)} + ---------------------------------------------------------->B + (S/NP) {\x.give(x,them,I)} + ------------------------------------------------------------------------------------------------------------> + N {\x.(give(x,them,I) & money(x))} + + +------- +Lexicon +------- + + >>> from nltk.ccg import lexicon + +Parse lexicon with semantics + + >>> print(str(lexicon.fromstring( + ... ''' + ... :- S,NP + ... + ... IntransVsg :: S\\NP[sg] + ... + ... sleeps => IntransVsg {\\x.sleep(x)} + ... eats => S\\NP[sg]/NP {\\x y.eat(x,y)} + ... + ... and => var\\var/var {\\x y.x & y} + ... ''', + ... True + ... ))) + and => ((_var0\_var0)/_var0) {(\x y.x & y)} + eats => ((S\NP['sg'])/NP) {\x y.eat(x,y)} + sleeps => (S\NP['sg']) {\x.sleep(x)} + +Parse lexicon without semantics + + >>> print(str(lexicon.fromstring( + ... ''' + ... :- S,NP + ... + ... IntransVsg :: S\\NP[sg] + ... + ... sleeps => IntransVsg + ... eats => S\\NP[sg]/NP {sem=\\x y.eat(x,y)} + ... + ... and => var\\var/var + ... ''', + ... False + ... ))) + and => ((_var0\_var0)/_var0) + eats => ((S\NP['sg'])/NP) + sleeps => (S\NP['sg']) + +Semantics are missing + + >>> print(str(lexicon.fromstring( + ... ''' + ... :- S,NP + ... + ... eats => S\\NP[sg]/NP + ... ''', + ... True + ... ))) + Traceback (most recent call last): + ... + AssertionError: eats => S\NP[sg]/NP must contain semantics because include_semantics is set to True + + +------------------------------------ +CCG combinator semantics computation +------------------------------------ + + >>> from nltk.sem.logic import * + >>> from nltk.ccg.logic import * + + >>> read_expr = Expression.fromstring + +Compute semantics from function application + + >>> print(str(compute_function_semantics(read_expr(r'\x.P(x)'), read_expr(r'book')))) + P(book) + + >>> print(str(compute_function_semantics(read_expr(r'\P.P(book)'), read_expr(r'read')))) + read(book) + + >>> print(str(compute_function_semantics(read_expr(r'\P.P(book)'), read_expr(r'\x.read(x)')))) + read(book) + +Compute semantics from composition + + >>> print(str(compute_composition_semantics(read_expr(r'\x.P(x)'), read_expr(r'\x.Q(x)')))) + \x.P(Q(x)) + + >>> print(str(compute_composition_semantics(read_expr(r'\x.P(x)'), read_expr(r'read')))) + Traceback (most recent call last): + ... + AssertionError: `read` must be a lambda expression + +Compute semantics from substitution + + >>> print(str(compute_substitution_semantics(read_expr(r'\x y.P(x,y)'), read_expr(r'\x.Q(x)')))) + \x.P(x,Q(x)) + + >>> print(str(compute_substitution_semantics(read_expr(r'\x.P(x)'), read_expr(r'read')))) + Traceback (most recent call last): + ... + AssertionError: `\x.P(x)` must be a lambda expression with 2 arguments + +Compute type-raise semantics + + >>> print(str(compute_type_raised_semantics(read_expr(r'\x.P(x)')))) + \F.F(\x.P(x)) + + >>> print(str(compute_type_raised_semantics(read_expr(r'\x.F(x)')))) + \G.G(\x.F(x)) + + >>> print(str(compute_type_raised_semantics(read_expr(r'\x y z.P(x,y,z)')))) + \F.F(\x y z.P(x,y,z)) diff --git a/nltk/test/chat80.doctest b/nltk/test/chat80.doctest new file mode 100644 index 0000000..12f8577 --- /dev/null +++ b/nltk/test/chat80.doctest @@ -0,0 +1,232 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +======= +Chat-80 +======= + +Chat-80 was a natural language system which allowed the user to +interrogate a Prolog knowledge base in the domain of world +geography. It was developed in the early '80s by Warren and Pereira; see +``_ for a description and +``_ for the source +files. + +The ``chat80`` module contains functions to extract data from the Chat-80 +relation files ('the world database'), and convert then into a format +that can be incorporated in the FOL models of +``nltk.sem.evaluate``. The code assumes that the Prolog +input files are available in the NLTK corpora directory. + +The Chat-80 World Database consists of the following files:: + + world0.pl + rivers.pl + cities.pl + countries.pl + contain.pl + borders.pl + +This module uses a slightly modified version of ``world0.pl``, in which +a set of Prolog rules have been omitted. The modified file is named +``world1.pl``. Currently, the file ``rivers.pl`` is not read in, since +it uses a list rather than a string in the second field. + +Reading Chat-80 Files +===================== + +Chat-80 relations are like tables in a relational database. The +relation acts as the name of the table; the first argument acts as the +'primary key'; and subsequent arguments are further fields in the +table. In general, the name of the table provides a label for a unary +predicate whose extension is all the primary keys. For example, +relations in ``cities.pl`` are of the following form:: + + 'city(athens,greece,1368).' + +Here, ``'athens'`` is the key, and will be mapped to a member of the +unary predicate *city*. + +By analogy with NLTK corpora, ``chat80`` defines a number of 'items' +which correspond to the relations. + + >>> from nltk.sem import chat80 + >>> print(chat80.items) + ('borders', 'circle_of_lat', 'circle_of_long', 'city', ...) + +The fields in the table are mapped to binary predicates. The first +argument of the predicate is the primary key, while the second +argument is the data in the relevant field. Thus, in the above +example, the third field is mapped to the binary predicate +*population_of*, whose extension is a set of pairs such as +``'(athens, 1368)'``. + +An exception to this general framework is required by the relations in +the files ``borders.pl`` and ``contains.pl``. These contain facts of the +following form:: + + 'borders(albania,greece).' + + 'contains0(africa,central_africa).' + +We do not want to form a unary concept out the element in +the first field of these records, and we want the label of the binary +relation just to be ``'border'``/``'contain'`` respectively. + +In order to drive the extraction process, we use 'relation metadata bundles' +which are Python dictionaries such as the following:: + + city = {'label': 'city', + 'closures': [], + 'schema': ['city', 'country', 'population'], + 'filename': 'cities.pl'} + +According to this, the file ``city['filename']`` contains a list of +relational tuples (or more accurately, the corresponding strings in +Prolog form) whose predicate symbol is ``city['label']`` and whose +relational schema is ``city['schema']``. The notion of a ``closure`` is +discussed in the next section. + +Concepts +======== +In order to encapsulate the results of the extraction, a class of +``Concept``\ s is introduced. A ``Concept`` object has a number of +attributes, in particular a ``prefLabel``, an arity and ``extension``. + + >>> c1 = chat80.Concept('dog', arity=1, extension=set(['d1', 'd2'])) + >>> print(c1) + Label = 'dog' + Arity = 1 + Extension = ['d1', 'd2'] + + + +The ``extension`` attribute makes it easier to inspect the output of +the extraction. + + >>> schema = ['city', 'country', 'population'] + >>> concepts = chat80.clause2concepts('cities.pl', 'city', schema) + >>> concepts + [Concept('city'), Concept('country_of'), Concept('population_of')] + >>> for c in concepts: + ... print("%s:\n\t%s" % (c.prefLabel, c.extension[:4])) + city: + ['athens', 'bangkok', 'barcelona', 'berlin'] + country_of: + [('athens', 'greece'), ('bangkok', 'thailand'), ('barcelona', 'spain'), ('berlin', 'east_germany')] + population_of: + [('athens', '1368'), ('bangkok', '1178'), ('barcelona', '1280'), ('berlin', '3481')] + +In addition, the ``extension`` can be further +processed: in the case of the ``'border'`` relation, we check that the +relation is **symmetric**, and in the case of the ``'contain'`` +relation, we carry out the **transitive closure**. The closure +properties associated with a concept is indicated in the relation +metadata, as indicated earlier. + + >>> borders = set([('a1', 'a2'), ('a2', 'a3')]) + >>> c2 = chat80.Concept('borders', arity=2, extension=borders) + >>> print(c2) + Label = 'borders' + Arity = 2 + Extension = [('a1', 'a2'), ('a2', 'a3')] + >>> c3 = chat80.Concept('borders', arity=2, closures=['symmetric'], extension=borders) + >>> c3.close() + >>> print(c3) + Label = 'borders' + Arity = 2 + Extension = [('a1', 'a2'), ('a2', 'a1'), ('a2', 'a3'), ('a3', 'a2')] + +The ``extension`` of a ``Concept`` object is then incorporated into a +``Valuation`` object. + +Persistence +=========== +The functions ``val_dump`` and ``val_load`` are provided to allow a +valuation to be stored in a persistent database and re-loaded, rather +than having to be re-computed each time. + +Individuals and Lexical Items +============================= +As well as deriving relations from the Chat-80 data, we also create a +set of individual constants, one for each entity in the domain. The +individual constants are string-identical to the entities. For +example, given a data item such as ``'zloty'``, we add to the valuation +a pair ``('zloty', 'zloty')``. In order to parse English sentences that +refer to these entities, we also create a lexical item such as the +following for each individual constant:: + + PropN[num=sg, sem=<\P.(P zloty)>] -> 'Zloty' + +The set of rules is written to the file ``chat_pnames.fcfg`` in the +current directory. + +SQL Query +========= + +The ``city`` relation is also available in RDB form and can be queried +using SQL statements. + + >>> import nltk + >>> q = "SELECT City, Population FROM city_table WHERE Country = 'china' and Population > 1000" + >>> for answer in chat80.sql_query('corpora/city_database/city.db', q): + ... print("%-10s %4s" % answer) + canton 1496 + chungking 1100 + mukden 1551 + peking 2031 + shanghai 5407 + tientsin 1795 + +The (deliberately naive) grammar ``sql.fcfg`` translates from English +to SQL: + + >>> nltk.data.show_cfg('grammars/book_grammars/sql0.fcfg') + % start S + S[SEM=(?np + WHERE + ?vp)] -> NP[SEM=?np] VP[SEM=?vp] + VP[SEM=(?v + ?pp)] -> IV[SEM=?v] PP[SEM=?pp] + VP[SEM=(?v + ?ap)] -> IV[SEM=?v] AP[SEM=?ap] + NP[SEM=(?det + ?n)] -> Det[SEM=?det] N[SEM=?n] + PP[SEM=(?p + ?np)] -> P[SEM=?p] NP[SEM=?np] + AP[SEM=?pp] -> A[SEM=?a] PP[SEM=?pp] + NP[SEM='Country="greece"'] -> 'Greece' + NP[SEM='Country="china"'] -> 'China' + Det[SEM='SELECT'] -> 'Which' | 'What' + N[SEM='City FROM city_table'] -> 'cities' + IV[SEM=''] -> 'are' + A[SEM=''] -> 'located' + P[SEM=''] -> 'in' + +Given this grammar, we can express, and then execute, queries in English. + + >>> cp = nltk.parse.load_parser('grammars/book_grammars/sql0.fcfg') + >>> query = 'What cities are in China' + >>> for tree in cp.parse(query.split()): + ... answer = tree.label()['SEM'] + ... q = " ".join(answer) + ... print(q) + ... + SELECT City FROM city_table WHERE Country="china" + + >>> rows = chat80.sql_query('corpora/city_database/city.db', q) + >>> for r in rows: print("%s" % r, end=' ') + canton chungking dairen harbin kowloon mukden peking shanghai sian tientsin + + +Using Valuations +----------------- + +In order to convert such an extension into a valuation, we use the +``make_valuation()`` method; setting ``read=True`` creates and returns +a new ``Valuation`` object which contains the results. + + >>> val = chat80.make_valuation(concepts, read=True) + >>> 'calcutta' in val['city'] + True + >>> [town for (town, country) in val['country_of'] if country == 'india'] + ['bombay', 'calcutta', 'delhi', 'hyderabad', 'madras'] + >>> dom = val.domain + >>> g = nltk.sem.Assignment(dom) + >>> m = nltk.sem.Model(dom, val) + >>> m.evaluate(r'population_of(jakarta, 533)', g) + True diff --git a/nltk/test/childes.doctest b/nltk/test/childes.doctest new file mode 100644 index 0000000..c0703fc --- /dev/null +++ b/nltk/test/childes.doctest @@ -0,0 +1,190 @@ +======================= + CHILDES Corpus Readers +======================= + +Read the XML version of the CHILDES corpus. + +Setup +===== + + >>> from nltk.test.childes_fixt import setup_module + >>> setup_module() + +How to use CHILDESCorpusReader +============================== + +Read the CHILDESCorpusReader class and read the CHILDES corpus saved in +the nltk_data directory. + + >>> import nltk + >>> from nltk.corpus.reader import CHILDESCorpusReader + >>> corpus_root = nltk.data.find('corpora/childes/data-xml/Eng-USA-MOR/') + +Reading files in the Valian corpus (Valian, 1991). + + >>> valian = CHILDESCorpusReader(corpus_root, 'Valian/.*.xml') + >>> valian.fileids() + ['Valian/01a.xml', 'Valian/01b.xml', 'Valian/02a.xml', 'Valian/02b.xml',... + +Count the number of files + + >>> len(valian.fileids()) + 43 + +Printing properties of the corpus files. + + >>> corpus_data = valian.corpus(valian.fileids()) + >>> print(corpus_data[0]['Lang']) + eng + >>> for key in sorted(corpus_data[0].keys()): + ... print(key, ": ", corpus_data[0][key]) + Corpus : valian + Date : 1986-03-04 + Id : 01a + Lang : eng + Version : 2.0.1 + {http://www.w3.org/2001/XMLSchema-instance}schemaLocation : http://www.talkbank.org/ns/talkbank http://talkbank.org/software/talkbank.xsd + +Printing information of participants of the corpus. The most common codes for +the participants are 'CHI' (target child), 'MOT' (mother), and 'INV' (investigator). + + >>> corpus_participants = valian.participants(valian.fileids()) + >>> for this_corpus_participants in corpus_participants[:2]: + ... for key in sorted(this_corpus_participants.keys()): + ... dct = this_corpus_participants[key] + ... print(key, ": ", [(k, dct[k]) for k in sorted(dct.keys())]) + CHI : [('age', 'P2Y1M3D'), ('group', 'normal'), ('id', 'CHI'), ('language', 'eng'), ('role', 'Target_Child'), ('sex', 'female')] + INV : [('id', 'INV'), ('language', 'eng'), ('role', 'Investigator')] + MOT : [('id', 'MOT'), ('language', 'eng'), ('role', 'Mother')] + CHI : [('age', 'P2Y1M12D'), ('group', 'normal'), ('id', 'CHI'), ('language', 'eng'), ('role', 'Target_Child'), ('sex', 'female')] + INV : [('id', 'INV'), ('language', 'eng'), ('role', 'Investigator')] + MOT : [('id', 'MOT'), ('language', 'eng'), ('role', 'Mother')] + +printing words. + + >>> valian.words('Valian/01a.xml') + ['at', 'Parent', "Lastname's", 'house', 'with', 'Child', 'Lastname', ... + +printing sentences. + + >>> valian.sents('Valian/01a.xml') + [['at', 'Parent', "Lastname's", 'house', 'with', 'Child', 'Lastname', + 'and', 'it', 'is', 'March', 'fourth', 'I', 'believe', 'and', 'when', + 'was', "Parent's", 'birthday'], ["Child's"], ['oh', "I'm", 'sorry'], + ["that's", 'okay'], ... + +You can specify the participants with the argument *speaker*. + + >>> valian.words('Valian/01a.xml',speaker=['INV']) + ['at', 'Parent', "Lastname's", 'house', 'with', 'Child', 'Lastname', ... + >>> valian.words('Valian/01a.xml',speaker=['MOT']) + ["Child's", "that's", 'okay', 'February', 'first', 'nineteen', ... + >>> valian.words('Valian/01a.xml',speaker=['CHI']) + ['tape', 'it', 'up', 'and', 'two', 'tape', 'players', 'have',... + + +tagged_words() and tagged_sents() return the usual (word,pos) tuple lists. +POS tags in the CHILDES are automatically assigned by MOR and POST programs +(MacWhinney, 2000). + + >>> valian.tagged_words('Valian/01a.xml')[:30] + [('at', 'prep'), ('Parent', 'n:prop'), ("Lastname's", 'n:prop'), ('house', 'n'), + ('with', 'prep'), ('Child', 'n:prop'), ('Lastname', 'n:prop'), ('and', 'coord'), + ('it', 'pro'), ('is', 'v:cop'), ('March', 'n:prop'), ('fourth', 'adj'), + ('I', 'pro:sub'), ('believe', 'v'), ('and', 'coord'), ('when', 'adv:wh'), + ('was', 'v:cop'), ("Parent's", 'n:prop'), ('birthday', 'n'), ("Child's", 'n:prop'), + ('oh', 'co'), ("I'm", 'pro:sub'), ('sorry', 'adj'), ("that's", 'pro:dem'), + ('okay', 'adj'), ('February', 'n:prop'), ('first', 'adj'), + ('nineteen', 'det:num'), ('eighty', 'det:num'), ('four', 'det:num')] + + >>> valian.tagged_sents('Valian/01a.xml')[:10] + [[('at', 'prep'), ('Parent', 'n:prop'), ("Lastname's", 'n:prop'), ('house', 'n'), + ('with', 'prep'), ('Child', 'n:prop'), ('Lastname', 'n:prop'), ('and', 'coord'), + ('it', 'pro'), ('is', 'v:cop'), ('March', 'n:prop'), ('fourth', 'adj'), + ('I', 'pro:sub'), ('believe', 'v'), ('and', 'coord'), ('when', 'adv:wh'), + ('was', 'v:cop'), ("Parent's", 'n:prop'), ('birthday', 'n')], + [("Child's", 'n:prop')], [('oh', 'co'), ("I'm", 'pro:sub'), ('sorry', 'adj')], + [("that's", 'pro:dem'), ('okay', 'adj')], + [('February', 'n:prop'), ('first', 'adj'), ('nineteen', 'det:num'), + ('eighty', 'det:num'), ('four', 'det:num')], + [('great', 'adj')], + [('and', 'coord'), ("she's", 'pro:sub'), ('two', 'det:num'), ('years', 'n'), ('old', 'adj')], + [('correct', 'adj')], + [('okay', 'co')], [('she', 'pro:sub'), ('just', 'adv:int'), ('turned', 'part'), ('two', 'det:num'), + ('a', 'det'), ('month', 'n'), ('ago', 'adv')]] + +When the argument *stem* is true, the word stems (e.g., 'is' -> 'be-3PS') are +used instead of the original words. + + >>> valian.words('Valian/01a.xml')[:30] + ['at', 'Parent', "Lastname's", 'house', 'with', 'Child', 'Lastname', 'and', 'it', 'is', ... + >>> valian.words('Valian/01a.xml',stem=True)[:30] + ['at', 'Parent', 'Lastname', 's', 'house', 'with', 'Child', 'Lastname', 'and', 'it', 'be-3S', ... + +When the argument *replace* is true, the replaced words are used instead of +the original words. + + >>> valian.words('Valian/01a.xml',speaker='CHI')[247] + 'tikteat' + >>> valian.words('Valian/01a.xml',speaker='CHI',replace=True)[247] + 'trick' + +When the argument *relation* is true, the relational relationships in the +sentence are returned. See Sagae et al. (2010) for details of the relational +structure adopted in the CHILDES. + + >>> valian.words('Valian/01a.xml',relation=True)[:10] + [[('at', 'prep', '1|0|ROOT'), ('Parent', 'n', '2|5|VOC'), ('Lastname', 'n', '3|5|MOD'), ('s', 'poss', '4|5|MOD'), ('house', 'n', '5|1|POBJ'), ('with', 'prep', '6|1|JCT'), ('Child', 'n', '7|8|NAME'), ('Lastname', 'n', '8|6|POBJ'), ('and', 'coord', '9|8|COORD'), ('it', 'pro', '10|11|SUBJ'), ('be-3S', 'v', '11|9|COMP'), ('March', 'n', '12|11|PRED'), ('fourth', 'adj', '13|12|MOD'), ('I', 'pro', '15|16|SUBJ'), ('believe', 'v', '16|14|ROOT'), ('and', 'coord', '18|17|ROOT'), ('when', 'adv', '19|20|PRED'), ('be-PAST', 'v', '20|18|COMP'), ('Parent', 'n', '21|23|MOD'), ('s', 'poss', '22|23|MOD'), ('birth', 'n', '23|20|SUBJ')], [('Child', 'n', '1|2|MOD'), ('s', 'poss', '2|0|ROOT')], [('oh', 'co', '1|4|COM'), ('I', 'pro', '3|4|SUBJ'), ('be', 'v', '4|0|ROOT'), ('sorry', 'adj', '5|4|PRED')], [('that', 'pro', '1|2|SUBJ'), ('be', 'v', '2|0|ROOT'), ('okay', 'adj', '3|2|PRED')], [('February', 'n', '1|6|VOC'), ('first', 'adj', '2|6|ENUM'), ('nineteen', 'det', '4|6|ENUM'), ('eighty', 'det', '5|6|ENUM'), ('four', 'det', '6|0|ROOT')], [('great', 'adj', '1|0|ROOT')], [('and', 'coord', '1|0|ROOT'), ('she', 'pro', '2|1|ROOT'), ('be', 'aux', '3|5|AUX'), ('two', 'det', '4|5|QUANT'), ('year-PL', 'n', '5|2|ROOT'), ('old', 'adj', '6|5|MOD')], [('correct', 'adj', '1|0|ROOT')], [('okay', 'co', '1|0|ROOT')], [('she', 'pro', '1|0|ROOT'), ('just', 'adv', '2|3|JCT'), ('turn-PERF', 'part', '3|1|XCOMP'), ('two', 'det', '4|6|QUANT'), ('a', 'det', '5|6|DET'), ('month', 'n', '6|3|OBJ'), ('ago', 'adv', '7|3|JCT')]] + +Printing age. When the argument *month* is true, the age information in +the CHILDES format is converted into the number of months. + + >>> valian.age() + ['P2Y1M3D', 'P2Y1M12D', 'P1Y9M21D', 'P1Y9M28D', 'P2Y1M23D', ... + >>> valian.age('Valian/01a.xml') + ['P2Y1M3D'] + >>> valian.age('Valian/01a.xml',month=True) + [25] + +Printing MLU. The criteria for the MLU computation is broadly based on +Brown (1973). + + >>> valian.MLU() + [2.3574660633484..., 2.292682926829..., 3.492857142857..., 2.961783439490..., + 2.0842696629213..., 3.169811320754..., 3.137404580152..., 3.0578034682080..., + 4.090163934426..., 3.488372093023..., 2.8773584905660..., 3.4792899408284..., + 4.0111940298507..., 3.456790123456..., 4.487603305785..., 4.007936507936..., + 5.25, 5.154696132596..., ...] + + >>> valian.MLU('Valian/01a.xml') + [2.35746606334...] + + +Basic stuff +============================== + +Count the number of words and sentences of each file. + + >>> valian = CHILDESCorpusReader(corpus_root, 'Valian/.*.xml') + >>> for this_file in valian.fileids()[:6]: + ... print(valian.corpus(this_file)[0]['Corpus'], valian.corpus(this_file)[0]['Id']) + ... print("num of words: %i" % len(valian.words(this_file))) + ... print("num of sents: %i" % len(valian.sents(this_file))) + valian 01a + num of words: 3606 + num of sents: 1027 + valian 01b + num of words: 4376 + num of sents: 1274 + valian 02a + num of words: 2673 + num of sents: 801 + valian 02b + num of words: 5020 + num of sents: 1583 + valian 03a + num of words: 2743 + num of sents: 988 + valian 03b + num of words: 4409 + num of sents: 1397 diff --git a/nltk/test/childes_fixt.py b/nltk/test/childes_fixt.py new file mode 100644 index 0000000..3e9a69e --- /dev/null +++ b/nltk/test/childes_fixt.py @@ -0,0 +1,13 @@ +def setup_module(): + import pytest + + import nltk.data + + try: + nltk.data.find("corpora/childes/data-xml/Eng-USA-MOR/") + except LookupError as e: + pytest.skip( + "The CHILDES corpus is not found. " + "It should be manually downloaded and saved/unpacked " + "to [NLTK_Data_Dir]/corpora/childes/" + ) diff --git a/nltk/test/chunk.doctest b/nltk/test/chunk.doctest new file mode 100644 index 0000000..386c2d2 --- /dev/null +++ b/nltk/test/chunk.doctest @@ -0,0 +1,372 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +========== + Chunking +========== + + >>> from nltk.chunk import * + >>> from nltk.chunk.util import * + >>> from nltk.chunk.regexp import * + >>> from nltk import Tree + + >>> tagged_text = "[ The/DT cat/NN ] sat/VBD on/IN [ the/DT mat/NN ] [ the/DT dog/NN ] chewed/VBD ./." + >>> gold_chunked_text = tagstr2tree(tagged_text) + >>> unchunked_text = gold_chunked_text.flatten() + +Chunking uses a special regexp syntax for rules that delimit the chunks. These +rules must be converted to 'regular' regular expressions before a sentence can +be chunked. + + >>> tag_pattern = "
    ?*" + >>> regexp_pattern = tag_pattern2re_pattern(tag_pattern) + >>> regexp_pattern + '(<(DT)>)?(<(JJ)>)*(<(NN[^\\{\\}<>]*)>)' + +Construct some new chunking rules. + + >>> chunk_rule = ChunkRule(r"<.*>+", "Chunk everything") + >>> strip_rule = StripRule(r"", "Strip on verbs/prepositions") + >>> split_rule = SplitRule("
    ", "
    ", + ... "Split successive determiner/noun pairs") + + +Create and score a series of chunk parsers, successively more complex. + + >>> chunk_parser = RegexpChunkParser([chunk_rule], chunk_label='NP') + >>> chunked_text = chunk_parser.parse(unchunked_text) + >>> print(chunked_text) + (S + (NP + The/DT + cat/NN + sat/VBD + on/IN + the/DT + mat/NN + the/DT + dog/NN + chewed/VBD + ./.)) + + >>> chunkscore = ChunkScore() + >>> chunkscore.score(gold_chunked_text, chunked_text) + >>> print(chunkscore.precision()) + 0.0 + + >>> print(chunkscore.recall()) + 0.0 + + >>> print(chunkscore.f_measure()) + 0 + + >>> for chunk in sorted(chunkscore.missed()): print(chunk) + (NP The/DT cat/NN) + (NP the/DT dog/NN) + (NP the/DT mat/NN) + + >>> for chunk in chunkscore.incorrect(): print(chunk) + (NP + The/DT + cat/NN + sat/VBD + on/IN + the/DT + mat/NN + the/DT + dog/NN + chewed/VBD + ./.) + + >>> chunk_parser = RegexpChunkParser([chunk_rule, strip_rule], + ... chunk_label='NP') + >>> chunked_text = chunk_parser.parse(unchunked_text) + >>> print(chunked_text) + (S + (NP The/DT cat/NN) + sat/VBD + on/IN + (NP the/DT mat/NN the/DT dog/NN) + chewed/VBD + ./.) + >>> assert chunked_text == chunk_parser.parse(list(unchunked_text)) + + >>> chunkscore = ChunkScore() + >>> chunkscore.score(gold_chunked_text, chunked_text) + >>> chunkscore.precision() + 0.5 + + >>> print(chunkscore.recall()) + 0.33333333... + + >>> print(chunkscore.f_measure()) + 0.4 + + >>> for chunk in sorted(chunkscore.missed()): print(chunk) + (NP the/DT dog/NN) + (NP the/DT mat/NN) + + >>> for chunk in chunkscore.incorrect(): print(chunk) + (NP the/DT mat/NN the/DT dog/NN) + + >>> chunk_parser = RegexpChunkParser([chunk_rule, strip_rule, split_rule], + ... chunk_label='NP') + >>> chunked_text = chunk_parser.parse(unchunked_text, trace=True) + # Input: +
    <.> + # Chunk everything: + {
    <.>} + # Strip on verbs/prepositions: + {
    } {
    } <.> + # Split successive determiner/noun pairs: + {
    } {
    }{
    } <.> + >>> print(chunked_text) + (S + (NP The/DT cat/NN) + sat/VBD + on/IN + (NP the/DT mat/NN) + (NP the/DT dog/NN) + chewed/VBD + ./.) + + >>> chunkscore = ChunkScore() + >>> chunkscore.score(gold_chunked_text, chunked_text) + >>> chunkscore.precision() + 1.0 + + >>> chunkscore.recall() + 1.0 + + >>> chunkscore.f_measure() + 1.0 + + >>> chunkscore.missed() + [] + + >>> chunkscore.incorrect() + [] + + >>> chunk_parser.rules() + [+'>, '>, + ', '
    '>] + +Printing parsers: + + >>> print(repr(chunk_parser)) + + >>> print(chunk_parser) + RegexpChunkParser with 3 rules: + Chunk everything + +'> + Strip on verbs/prepositions + '> + Split successive determiner/noun pairs + ', '
    '> + +Regression Tests +~~~~~~~~~~~~~~~~ +ChunkParserI +------------ +`ChunkParserI` is an abstract interface -- it is not meant to be +instantiated directly. + + >>> ChunkParserI().parse([]) + Traceback (most recent call last): + . . . + NotImplementedError + + +ChunkString +----------- +ChunkString can be built from a tree of tagged tuples, a tree of +trees, or a mixed list of both: + + >>> t1 = Tree('S', [('w%d' % i, 't%d' % i) for i in range(10)]) + >>> t2 = Tree('S', [Tree('t0', []), Tree('t1', ['c1'])]) + >>> t3 = Tree('S', [('w0', 't0'), Tree('t1', ['c1'])]) + >>> ChunkString(t1) + '> + >>> ChunkString(t2) + '> + >>> ChunkString(t3) + '> + +Other values generate an error: + + >>> ChunkString(Tree('S', ['x'])) + Traceback (most recent call last): + . . . + ValueError: chunk structures must contain tagged tokens or trees + +The `str()` for a chunk string adds spaces to it, which makes it line +up with `str()` output for other chunk strings over the same +underlying input. + + >>> cs = ChunkString(t1) + >>> print(cs) + + >>> cs.xform('', '{}') + >>> print(cs) + {} + +The `_verify()` method makes sure that our transforms don't corrupt +the chunk string. By setting debug_level=2, `_verify()` will be +called at the end of every call to `xform`. + + >>> cs = ChunkString(t1, debug_level=3) + + >>> # tag not marked with <...>: + >>> cs.xform('', 't3') + Traceback (most recent call last): + . . . + ValueError: Transformation generated invalid chunkstring: + t3 + + >>> # brackets not balanced: + >>> cs.xform('', '{') + Traceback (most recent call last): + . . . + ValueError: Transformation generated invalid chunkstring: + { + + >>> # nested brackets: + >>> cs.xform('', '{{}}') + Traceback (most recent call last): + . . . + ValueError: Transformation generated invalid chunkstring: + {{}} + + >>> # modified tags: + >>> cs.xform('', '') + Traceback (most recent call last): + . . . + ValueError: Transformation generated invalid chunkstring: tag changed + + >>> # added tags: + >>> cs.xform('', '') + Traceback (most recent call last): + . . . + ValueError: Transformation generated invalid chunkstring: tag changed + +Chunking Rules +-------------- + +Test the different rule constructors & __repr__ methods: + + >>> r1 = RegexpChunkRule(''+ChunkString.IN_STRIP_PATTERN, + ... '{}', 'chunk and ') + >>> r2 = RegexpChunkRule(re.compile(''+ChunkString.IN_STRIP_PATTERN), + ... '{}', 'chunk and ') + >>> r3 = ChunkRule('', 'chunk and ') + >>> r4 = StripRule('', 'strip and ') + >>> r5 = UnChunkRule('', 'unchunk and ') + >>> r6 = MergeRule('', '', 'merge w/ ') + >>> r7 = SplitRule('', '', 'split from ') + >>> r8 = ExpandLeftRule('', '', 'expand left ') + >>> r9 = ExpandRightRule('', '', 'expand right ') + >>> for rule in r1, r2, r3, r4, r5, r6, r7, r8, r9: + ... print(rule) + (?=[^\\}]*(\\{|$))'->'{}'> + (?=[^\\}]*(\\{|$))'->'{}'> + '> + '> + '> + ', ''> + ', ''> + ', ''> + ', ''> + +`tag_pattern2re_pattern()` complains if the tag pattern looks problematic: + + >>> tag_pattern2re_pattern('{}') + Traceback (most recent call last): + . . . + ValueError: Bad tag pattern: '{}' + +RegexpChunkParser +----------------- + +A warning is printed when parsing an empty sentence: + + >>> parser = RegexpChunkParser([ChunkRule('', '')]) + >>> parser.parse(Tree('S', [])) + Warning: parsing empty text + Tree('S', []) + +RegexpParser +------------ + + >>> parser = RegexpParser(''' + ... NP: {
    ? * *} # NP + ... P: {} # Preposition + ... V: {} # Verb + ... PP: {

    } # PP -> P NP + ... VP: { *} # VP -> V (NP|PP)* + ... ''') + >>> print(repr(parser)) + + >>> print(parser) + chunk.RegexpParser with 5 stages: + RegexpChunkParser with 1 rules: + NP ? * *'> + RegexpChunkParser with 1 rules: + Preposition '> + RegexpChunkParser with 1 rules: + Verb '> + RegexpChunkParser with 1 rules: + PP -> P NP '> + RegexpChunkParser with 1 rules: + VP -> V (NP|PP)* *'> + >>> print(parser.parse(unchunked_text, trace=True)) + # Input: +

    <.> + # NP: + {
    } {
    }{
    } <.> + # Input: + <.> + # Preposition: + {} <.> + # Input: +

    <.> + # Verb: + {}

    {} <.> + # Input: +

    <.> + # PP -> P NP: + {

    } <.> + # Input: + <.> + # VP -> V (NP|PP)*: + { }{} <.> + (S + (NP The/DT cat/NN) + (VP + (V sat/VBD) + (PP (P on/IN) (NP the/DT mat/NN)) + (NP the/DT dog/NN)) + (VP (V chewed/VBD)) + ./.) + +Test parsing of other rule types: + + >>> print(RegexpParser(''' + ... X: + ... }{ # strip rule + ... }{ # split rule + ... {} # merge rule + ... {} # chunk rule w/ context + ... ''')) + chunk.RegexpParser with 1 stages: + RegexpChunkParser with 4 rules: + strip rule '> + split rule ', ''> + merge rule ', ''> + chunk rule w/ context ', '', ''> + +Illegal patterns give an error message: + + >>> print(RegexpParser('X: {} {}')) + Traceback (most recent call last): + . . . + ValueError: Illegal chunk pattern: {} {} diff --git a/nltk/test/classify.doctest b/nltk/test/classify.doctest new file mode 100644 index 0000000..a38b30d --- /dev/null +++ b/nltk/test/classify.doctest @@ -0,0 +1,202 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +============= + Classifiers +============= + + >>> from nltk.test.classify_fixt import setup_module + >>> setup_module() + +Classifiers label tokens with category labels (or *class labels*). +Typically, labels are represented with strings (such as ``"health"`` +or ``"sports"``. In NLTK, classifiers are defined using classes that +implement the `ClassifierI` interface, which supports the following operations: + +- self.classify(featureset) +- self.classify_many(featuresets) +- self.labels() +- self.prob_classify(featureset) +- self.prob_classify_many(featuresets) + +NLTK defines several classifier classes: + +- `ConditionalExponentialClassifier` +- `DecisionTreeClassifier` +- `MaxentClassifier` +- `NaiveBayesClassifier` +- `WekaClassifier` + +Classifiers are typically created by training them on a training +corpus. + + +Regression Tests +~~~~~~~~~~~~~~~~ + +We define a very simple training corpus with 3 binary features: ['a', +'b', 'c'], and are two labels: ['x', 'y']. We use a simple feature set so +that the correct answers can be calculated analytically (although we +haven't done this yet for all tests). + + >>> import nltk + >>> train = [ + ... (dict(a=1,b=1,c=1), 'y'), + ... (dict(a=1,b=1,c=1), 'x'), + ... (dict(a=1,b=1,c=0), 'y'), + ... (dict(a=0,b=1,c=1), 'x'), + ... (dict(a=0,b=1,c=1), 'y'), + ... (dict(a=0,b=0,c=1), 'y'), + ... (dict(a=0,b=1,c=0), 'x'), + ... (dict(a=0,b=0,c=0), 'x'), + ... (dict(a=0,b=1,c=1), 'y'), + ... (dict(a=None,b=1,c=0), 'x'), + ... ] + >>> test = [ + ... (dict(a=1,b=0,c=1)), # unseen + ... (dict(a=1,b=0,c=0)), # unseen + ... (dict(a=0,b=1,c=1)), # seen 3 times, labels=y,y,x + ... (dict(a=0,b=1,c=0)), # seen 1 time, label=x + ... ] + +Test the Naive Bayes classifier: + + >>> classifier = nltk.classify.NaiveBayesClassifier.train(train) + >>> sorted(classifier.labels()) + ['x', 'y'] + >>> classifier.classify_many(test) + ['y', 'x', 'y', 'x'] + >>> for pdist in classifier.prob_classify_many(test): + ... print('%.4f %.4f' % (pdist.prob('x'), pdist.prob('y'))) + 0.2500 0.7500 + 0.5833 0.4167 + 0.3571 0.6429 + 0.7000 0.3000 + >>> classifier.show_most_informative_features() + Most Informative Features + c = 0 x : y = 2.3 : 1.0 + c = 1 y : x = 1.8 : 1.0 + a = 1 y : x = 1.7 : 1.0 + a = 0 x : y = 1.0 : 1.0 + b = 0 x : y = 1.0 : 1.0 + b = 1 x : y = 1.0 : 1.0 + +Test the Decision Tree classifier (without None): + + >>> classifier = nltk.classify.DecisionTreeClassifier.train( + ... train[:-1], entropy_cutoff=0, + ... support_cutoff=0) + >>> sorted(classifier.labels()) + ['x', 'y'] + >>> print(classifier) + c=0? .................................................. x + a=0? ................................................ x + a=1? ................................................ y + c=1? .................................................. y + + >>> classifier.classify_many(test) + ['y', 'y', 'y', 'x'] + >>> for pdist in classifier.prob_classify_many(test): + ... print('%.4f %.4f' % (pdist.prob('x'), pdist.prob('y'))) + Traceback (most recent call last): + . . . + NotImplementedError + + +Test the Decision Tree classifier (with None): + + >>> classifier = nltk.classify.DecisionTreeClassifier.train( + ... train, entropy_cutoff=0, + ... support_cutoff=0) + >>> sorted(classifier.labels()) + ['x', 'y'] + >>> print(classifier) + c=0? .................................................. x + a=0? ................................................ x + a=1? ................................................ y + a=None? ............................................. x + c=1? .................................................. y + + + +Test SklearnClassifier, which requires the scikit-learn package. + + >>> from nltk.classify import SklearnClassifier + >>> from sklearn.naive_bayes import BernoulliNB + >>> from sklearn.svm import SVC + >>> train_data = [({"a": 4, "b": 1, "c": 0}, "ham"), + ... ({"a": 5, "b": 2, "c": 1}, "ham"), + ... ({"a": 0, "b": 3, "c": 4}, "spam"), + ... ({"a": 5, "b": 1, "c": 1}, "ham"), + ... ({"a": 1, "b": 4, "c": 3}, "spam")] + >>> classif = SklearnClassifier(BernoulliNB()).train(train_data) + >>> test_data = [{"a": 3, "b": 2, "c": 1}, + ... {"a": 0, "b": 3, "c": 7}] + >>> classif.classify_many(test_data) + ['ham', 'spam'] + >>> classif = SklearnClassifier(SVC(), sparse=False).train(train_data) + >>> classif.classify_many(test_data) + ['ham', 'spam'] + +Test the Maximum Entropy classifier training algorithms; they should all +generate the same results. + + >>> def print_maxent_test_header(): + ... print(' '*11+''.join([' test[%s] ' % i + ... for i in range(len(test))])) + ... print(' '*11+' p(x) p(y)'*len(test)) + ... print('-'*(11+15*len(test))) + + >>> def test_maxent(algorithm): + ... print('%11s' % algorithm, end=' ') + ... try: + ... classifier = nltk.classify.MaxentClassifier.train( + ... train, algorithm, trace=0, max_iter=1000) + ... except Exception as e: + ... print('Error: %r' % e) + ... return + ... + ... for featureset in test: + ... pdist = classifier.prob_classify(featureset) + ... print('%8.2f%6.2f' % (pdist.prob('x'), pdist.prob('y')), end=' ') + ... print() + + >>> print_maxent_test_header(); test_maxent('GIS'); test_maxent('IIS') + test[0] test[1] test[2] test[3] + p(x) p(y) p(x) p(y) p(x) p(y) p(x) p(y) + ----------------------------------------------------------------------- + GIS 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 + IIS 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 + + >>> test_maxent('MEGAM'); test_maxent('TADM') # doctest: +SKIP + MEGAM 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 + TADM 0.16 0.84 0.46 0.54 0.41 0.59 0.76 0.24 + + + +Regression tests for TypedMaxentFeatureEncoding +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + >>> from nltk.classify import maxent + >>> train = [ + ... ({'a': 1, 'b': 1, 'c': 1}, 'y'), + ... ({'a': 5, 'b': 5, 'c': 5}, 'x'), + ... ({'a': 0.9, 'b': 0.9, 'c': 0.9}, 'y'), + ... ({'a': 5.5, 'b': 5.4, 'c': 5.3}, 'x'), + ... ({'a': 0.8, 'b': 1.2, 'c': 1}, 'y'), + ... ({'a': 5.1, 'b': 4.9, 'c': 5.2}, 'x') + ... ] + + >>> test = [ + ... {'a': 1, 'b': 0.8, 'c': 1.2}, + ... {'a': 5.2, 'b': 5.1, 'c': 5} + ... ] + + >>> encoding = maxent.TypedMaxentFeatureEncoding.train( + ... train, count_cutoff=3, alwayson_features=True) + + >>> classifier = maxent.MaxentClassifier.train( + ... train, bernoulli=False, encoding=encoding, trace=0) + + >>> classifier.classify_many(test) + ['y', 'x'] diff --git a/nltk/test/classify_fixt.py b/nltk/test/classify_fixt.py new file mode 100644 index 0000000..3ee9e74 --- /dev/null +++ b/nltk/test/classify_fixt.py @@ -0,0 +1,5 @@ +# most of classify.doctest requires numpy +def setup_module(): + import pytest + + pytest.importorskip("numpy") diff --git a/nltk/test/collections.doctest b/nltk/test/collections.doctest new file mode 100644 index 0000000..f2f7789 --- /dev/null +++ b/nltk/test/collections.doctest @@ -0,0 +1,31 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +=========== +Collections +=========== + + >>> import nltk + >>> from nltk.collections import * + +Trie +---- + +Trie can be pickled: + + >>> import pickle + >>> trie = nltk.collections.Trie(['a']) + >>> s = pickle.dumps(trie) + >>> pickle.loads(s) + {'a': {True: None}} + +LazyIteratorList +---------------- + +Fetching the length of a LazyIteratorList object does not throw a StopIteration exception: + + >>> lil = LazyIteratorList(i for i in range(1, 11)) + >>> lil[-1] + 10 + >>> len(lil) + 10 diff --git a/nltk/test/collocations.doctest b/nltk/test/collocations.doctest new file mode 100644 index 0000000..6f6fcc6 --- /dev/null +++ b/nltk/test/collocations.doctest @@ -0,0 +1,307 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +============== + Collocations +============== + +Overview +~~~~~~~~ + +Collocations are expressions of multiple words which commonly co-occur. For +example, the top ten bigram collocations in Genesis are listed below, as +measured using Pointwise Mutual Information. + + >>> import nltk + >>> from nltk.collocations import * + >>> bigram_measures = nltk.collocations.BigramAssocMeasures() + >>> trigram_measures = nltk.collocations.TrigramAssocMeasures() + >>> fourgram_measures = nltk.collocations.QuadgramAssocMeasures() + >>> finder = BigramCollocationFinder.from_words( + ... nltk.corpus.genesis.words('english-web.txt')) + >>> finder.nbest(bigram_measures.pmi, 10) + [('Allon', 'Bacuth'), ('Ashteroth', 'Karnaim'), ('Ben', 'Ammi'), + ('En', 'Mishpat'), ('Jegar', 'Sahadutha'), ('Salt', 'Sea'), + ('Whoever', 'sheds'), ('appoint', 'overseers'), ('aromatic', 'resin'), + ('cutting', 'instrument')] + +While these words are highly collocated, the expressions are also very +infrequent. Therefore it is useful to apply filters, such as ignoring all +bigrams which occur less than three times in the corpus: + + >>> finder.apply_freq_filter(3) + >>> finder.nbest(bigram_measures.pmi, 10) + [('Beer', 'Lahai'), ('Lahai', 'Roi'), ('gray', 'hairs'), + ('ewe', 'lambs'), ('Most', 'High'), ('many', 'colors'), + ('burnt', 'offering'), ('Paddan', 'Aram'), ('east', 'wind'), + ('living', 'creature')] + +We may similarly find collocations among tagged words: + + >>> finder = BigramCollocationFinder.from_words( + ... nltk.corpus.brown.tagged_words('ca01', tagset='universal')) + >>> finder.nbest(bigram_measures.pmi, 5) + [(('1,119', 'NUM'), ('votes', 'NOUN')), + (('1962', 'NUM'), ("governor's", 'NOUN')), + (('637', 'NUM'), ('E.', 'NOUN')), + (('Alpharetta', 'NOUN'), ('prison', 'NOUN')), + (('Bar', 'NOUN'), ('Association', 'NOUN'))] + +Or tags alone: + + >>> finder = BigramCollocationFinder.from_words(t for w, t in + ... nltk.corpus.brown.tagged_words('ca01', tagset='universal')) + >>> finder.nbest(bigram_measures.pmi, 10) + [('PRT', 'VERB'), ('PRON', 'VERB'), ('ADP', 'DET'), ('.', 'PRON'), ('DET', 'ADJ'), + ('CONJ', 'PRON'), ('ADP', 'NUM'), ('NUM', '.'), ('ADV', 'ADV'), ('VERB', 'ADV')] + +Or spanning intervening words: + + >>> finder = BigramCollocationFinder.from_words( + ... nltk.corpus.genesis.words('english-web.txt'), + ... window_size = 20) + >>> finder.apply_freq_filter(2) + >>> ignored_words = nltk.corpus.stopwords.words('english') + >>> finder.apply_word_filter(lambda w: len(w) < 3 or w.lower() in ignored_words) + >>> finder.nbest(bigram_measures.likelihood_ratio, 10) + [('chief', 'chief'), ('became', 'father'), ('years', 'became'), + ('hundred', 'years'), ('lived', 'became'), ('king', 'king'), + ('lived', 'years'), ('became', 'became'), ('chief', 'chiefs'), + ('hundred', 'became')] + +Finders +~~~~~~~ + +The collocations package provides collocation finders which by default +consider all ngrams in a text as candidate collocations: + + >>> text = "I do not like green eggs and ham, I do not like them Sam I am!" + >>> tokens = nltk.wordpunct_tokenize(text) + >>> finder = BigramCollocationFinder.from_words(tokens) + >>> scored = finder.score_ngrams(bigram_measures.raw_freq) + >>> sorted(bigram for bigram, score in scored) + [(',', 'I'), ('I', 'am'), ('I', 'do'), ('Sam', 'I'), ('am', '!'), + ('and', 'ham'), ('do', 'not'), ('eggs', 'and'), ('green', 'eggs'), + ('ham', ','), ('like', 'green'), ('like', 'them'), ('not', 'like'), + ('them', 'Sam')] + +We could otherwise construct the collocation finder from manually-derived +FreqDists: + + >>> word_fd = nltk.FreqDist(tokens) + >>> bigram_fd = nltk.FreqDist(nltk.bigrams(tokens)) + >>> finder = BigramCollocationFinder(word_fd, bigram_fd) + >>> scored == finder.score_ngrams(bigram_measures.raw_freq) + True + +A similar interface is provided for trigrams: + + >>> finder = TrigramCollocationFinder.from_words(tokens) + >>> scored = finder.score_ngrams(trigram_measures.raw_freq) + >>> set(trigram for trigram, score in scored) == set(nltk.trigrams(tokens)) + True + +We may want to select only the top n results: + + >>> sorted(finder.nbest(trigram_measures.raw_freq, 2)) + [('I', 'do', 'not'), ('do', 'not', 'like')] + +Alternatively, we can select those above a minimum score value: + + >>> sorted(finder.above_score(trigram_measures.raw_freq, + ... 1.0 / len(tuple(nltk.trigrams(tokens))))) + [('I', 'do', 'not'), ('do', 'not', 'like')] + +Now spanning intervening words: + + >>> finder = TrigramCollocationFinder.from_words(tokens) + >>> finder = TrigramCollocationFinder.from_words(tokens, window_size=4) + >>> sorted(finder.nbest(trigram_measures.raw_freq, 4)) + [('I', 'do', 'like'), ('I', 'do', 'not'), ('I', 'not', 'like'), ('do', 'not', 'like')] + +A closer look at the finder's ngram frequencies: + + >>> sorted(finder.ngram_fd.items(), key=lambda t: (-t[1], t[0]))[:10] + [(('I', 'do', 'like'), 2), (('I', 'do', 'not'), 2), (('I', 'not', 'like'), 2), + (('do', 'not', 'like'), 2), ((',', 'I', 'do'), 1), ((',', 'I', 'not'), 1), + ((',', 'do', 'not'), 1), (('I', 'am', '!'), 1), (('Sam', 'I', '!'), 1), + (('Sam', 'I', 'am'), 1)] + +A similar interface is provided for fourgrams: + + >>> finder_4grams = QuadgramCollocationFinder.from_words(tokens) + >>> scored_4grams = finder_4grams.score_ngrams(fourgram_measures.raw_freq) + >>> set(fourgram for fourgram, score in scored_4grams) == set(nltk.ngrams(tokens, n=4)) + True + +Filtering candidates +~~~~~~~~~~~~~~~~~~~~ + +All the ngrams in a text are often too many to be useful when finding +collocations. It is generally useful to remove some words or punctuation, +and to require a minimum frequency for candidate collocations. + +Given our sample text above, if we remove all trigrams containing personal +pronouns from candidature, score_ngrams should return 6 less results, and +'do not like' will be the only candidate which occurs more than once: + + >>> finder = TrigramCollocationFinder.from_words(tokens) + >>> len(finder.score_ngrams(trigram_measures.raw_freq)) + 14 + >>> finder.apply_word_filter(lambda w: w in ('I', 'me')) + >>> len(finder.score_ngrams(trigram_measures.raw_freq)) + 8 + >>> sorted(finder.above_score(trigram_measures.raw_freq, + ... 1.0 / len(tuple(nltk.trigrams(tokens))))) + [('do', 'not', 'like')] + +Sometimes a filter is a function on the whole ngram, rather than each word, +such as if we may permit 'and' to appear in the middle of a trigram, but +not on either edge: + + >>> finder.apply_ngram_filter(lambda w1, w2, w3: 'and' in (w1, w3)) + >>> len(finder.score_ngrams(trigram_measures.raw_freq)) + 6 + +Finally, it is often important to remove low frequency candidates, as we +lack sufficient evidence about their significance as collocations: + + >>> finder.apply_freq_filter(2) + >>> len(finder.score_ngrams(trigram_measures.raw_freq)) + 1 + +Association measures +~~~~~~~~~~~~~~~~~~~~ + +A number of measures are available to score collocations or other associations. +The arguments to measure functions are marginals of a contingency table, in the +bigram case (n_ii, (n_ix, n_xi), n_xx):: + + w1 ~w1 + ------ ------ + w2 | n_ii | n_oi | = n_xi + ------ ------ + ~w2 | n_io | n_oo | + ------ ------ + = n_ix TOTAL = n_xx + +We test their calculation using some known values presented in Manning and +Schutze's text and other papers. + +Student's t: examples from Manning and Schutze 5.3.2 + + >>> print('%0.4f' % bigram_measures.student_t(8, (15828, 4675), 14307668)) + 0.9999 + >>> print('%0.4f' % bigram_measures.student_t(20, (42, 20), 14307668)) + 4.4721 + +Chi-square: examples from Manning and Schutze 5.3.3 + + >>> print('%0.2f' % bigram_measures.chi_sq(8, (15828, 4675), 14307668)) + 1.55 + >>> print('%0.0f' % bigram_measures.chi_sq(59, (67, 65), 571007)) + 456400 + +Likelihood ratios: examples from Dunning, CL, 1993 + + >>> print('%0.2f' % bigram_measures.likelihood_ratio(110, (2552, 221), 31777)) + 270.72 + >>> print('%0.2f' % bigram_measures.likelihood_ratio(8, (13, 32), 31777)) + 95.29 + +Pointwise Mutual Information: examples from Manning and Schutze 5.4 + + >>> print('%0.2f' % bigram_measures.pmi(20, (42, 20), 14307668)) + 18.38 + >>> print('%0.2f' % bigram_measures.pmi(20, (15019, 15629), 14307668)) + 0.29 + +TODO: Find authoritative results for trigrams. + +Using contingency table values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While frequency counts make marginals readily available for collocation +finding, it is common to find published contingency table values. The +collocations package therefore provides a wrapper, ContingencyMeasures, which +wraps an association measures class, providing association measures which +take contingency values as arguments, (n_ii, n_io, n_oi, n_oo) in the +bigram case. + + >>> from nltk.metrics import ContingencyMeasures + >>> cont_bigram_measures = ContingencyMeasures(bigram_measures) + >>> print('%0.2f' % cont_bigram_measures.likelihood_ratio(8, 5, 24, 31740)) + 95.29 + >>> print('%0.2f' % cont_bigram_measures.chi_sq(8, 15820, 4667, 14287173)) + 1.55 + +Ranking and correlation +~~~~~~~~~~~~~~~~~~~~~~~ + +It is useful to consider the results of finding collocations as a ranking, and +the rankings output using different association measures can be compared using +the Spearman correlation coefficient. + +Ranks can be assigned to a sorted list of results trivially by assigning +strictly increasing ranks to each result: + + >>> from nltk.metrics.spearman import * + >>> results_list = ['item1', 'item2', 'item3', 'item4', 'item5'] + >>> print(list(ranks_from_sequence(results_list))) + [('item1', 0), ('item2', 1), ('item3', 2), ('item4', 3), ('item5', 4)] + +If scores are available for each result, we may allow sufficiently similar +results (differing by no more than rank_gap) to be assigned the same rank: + + >>> results_scored = [('item1', 50.0), ('item2', 40.0), ('item3', 38.0), + ... ('item4', 35.0), ('item5', 14.0)] + >>> print(list(ranks_from_scores(results_scored, rank_gap=5))) + [('item1', 0), ('item2', 1), ('item3', 1), ('item4', 1), ('item5', 4)] + +The Spearman correlation coefficient gives a number from -1.0 to 1.0 comparing +two rankings. A coefficient of 1.0 indicates identical rankings; -1.0 indicates +exact opposite rankings. + + >>> print('%0.1f' % spearman_correlation( + ... ranks_from_sequence(results_list), + ... ranks_from_sequence(results_list))) + 1.0 + >>> print('%0.1f' % spearman_correlation( + ... ranks_from_sequence(reversed(results_list)), + ... ranks_from_sequence(results_list))) + -1.0 + >>> results_list2 = ['item2', 'item3', 'item1', 'item5', 'item4'] + >>> print('%0.1f' % spearman_correlation( + ... ranks_from_sequence(results_list), + ... ranks_from_sequence(results_list2))) + 0.6 + >>> print('%0.1f' % spearman_correlation( + ... ranks_from_sequence(reversed(results_list)), + ... ranks_from_sequence(results_list2))) + -0.6 + +Keywords +~~~~~~~~ + +Bigram association metrics can also be used to perform keyword analysis. . For example, this finds the keywords +associated with the "romance" section of the Brown corpus as measured by likelihood ratio: + + >>> romance = nltk.FreqDist(w.lower() for w in nltk.corpus.brown.words(categories='romance') if w.isalpha()) + >>> freq = nltk.FreqDist(w.lower() for w in nltk.corpus.brown.words() if w.isalpha()) + + >>> key = nltk.FreqDist() + >>> for w in romance: + ... key[w] = bigram_measures.likelihood_ratio(romance[w], (freq[w], romance.N()), freq.N()) + + >>> for k,v in key.most_common(10): + ... print(f'{k:10s} {v:9.3f}') + she 1163.325 + i 995.961 + her 930.528 + you 513.149 + of 501.891 + is 463.386 + had 421.615 + he 411.000 + the 347.632 + said 300.811 diff --git a/nltk/test/concordance.doctest b/nltk/test/concordance.doctest new file mode 100644 index 0000000..c24c3f2 --- /dev/null +++ b/nltk/test/concordance.doctest @@ -0,0 +1,75 @@ +.. Copyright (C) 2001-2016 NLTK Project +.. For license information, see LICENSE.TXT + +================================== +Concordance Example +================================== + +A concordance view shows us every occurrence of a given +word, together with some context. Here we look up the word monstrous +in Moby Dick by entering text1 followed by a period, then the term +concordance, and then placing "monstrous" in parentheses: + +>>> from nltk.corpus import gutenberg +>>> from nltk.text import Text +>>> corpus = gutenberg.words('melville-moby_dick.txt') +>>> text = Text(corpus) + +>>> text.concordance("monstrous") +Displaying 11 of 11 matches: +ong the former , one was of a most monstrous size . ... This came towards us , +ON OF THE PSALMS . " Touching that monstrous bulk of the whale or ork we have r +ll over with a heathenish array of monstrous clubs and spears . Some were thick +d as you gazed , and wondered what monstrous cannibal and savage could ever hav +that has survived the flood ; most monstrous and most mountainous ! That Himmal +they might scout at Moby Dick as a monstrous fable , or still worse and more de +th of Radney .'" CHAPTER 55 Of the Monstrous Pictures of Whales . I shall ere l +ing Scenes . In connexion with the monstrous pictures of whales , I am strongly +ere to enter upon those still more monstrous stories of them which are to be fo +ght have been rummaged out of this monstrous cabinet there is no telling . But +of Whale - Bones ; for Whales of a monstrous size are oftentimes cast up dead u + +>>> text.concordance("monstrous") +Displaying 11 of 11 matches: +ong the former , one was of a most monstrous size . ... This came towards us , +ON OF THE PSALMS . " Touching that monstrous bulk of the whale or ork we have r +ll over with a heathenish array of monstrous clubs and spears . Some were thick +... + +We can also search for a multi-word phrase by passing a list of strings: + +>>> text.concordance(["monstrous", "size"]) +Displaying 2 of 2 matches: +the former , one was of a most monstrous size . ... This came towards us , op +Whale - Bones ; for Whales of a monstrous size are oftentimes cast up dead upo + +================================= +Concordance List +================================= + +Often we need to store the results of concordance for further usage. +To do so, call the concordance function with the stdout argument set +to false: + +>>> from nltk.corpus import gutenberg +>>> from nltk.text import Text +>>> corpus = gutenberg.words('melville-moby_dick.txt') +>>> text = Text(corpus) +>>> con_list = text.concordance_list("monstrous") +>>> con_list[2].line +'ll over with a heathenish array of monstrous clubs and spears . Some were thick' +>>> len(con_list) +11 + +================================= +Patching Issue #2088 +================================= + +Patching https://github.com/nltk/nltk/issues/2088 +The left slice of the left context should be clip to 0 if the `i-context` < 0. + +>>> from nltk import Text, word_tokenize +>>> jane_eyre = 'Chapter 1\nTHERE was no possibility of taking a walk that day. We had been wandering, indeed, in the leafless shrubbery an hour in the morning; but since dinner (Mrs. Reed, when there was no company, dined early) the cold winter wind had brought with it clouds so sombre, and a rain so penetrating, that further outdoor exercise was now out of the question.' +>>> text = Text(word_tokenize(jane_eyre)) +>>> text.concordance_list('taking')[0].left +['Chapter', '1', 'THERE', 'was', 'no', 'possibility', 'of'] diff --git a/nltk/test/conftest.py b/nltk/test/conftest.py new file mode 100644 index 0000000..5027009 --- /dev/null +++ b/nltk/test/conftest.py @@ -0,0 +1,35 @@ +import sys + +import pytest + +from nltk.corpus.reader import CorpusReader + + +@pytest.fixture(autouse=True) +def mock_plot(mocker): + """Disable matplotlib plotting in test code""" + + try: + import matplotlib.pyplot as plt + + mocker.patch.object(plt, "gca") + mocker.patch.object(plt, "show") + except ImportError: + pass + + +@pytest.fixture(scope="module", autouse=True) +def teardown_loaded_corpora(): + """ + After each test session ends (either doctest or unit test), + unload any loaded corpora + """ + + yield # first, wait for the test to end + + import nltk.corpus + + for name in dir(nltk.corpus): + obj = getattr(nltk.corpus, name, None) + if isinstance(obj, CorpusReader) and hasattr(obj, "_unload"): + obj._unload() diff --git a/nltk/test/corpus.doctest b/nltk/test/corpus.doctest new file mode 100644 index 0000000..42598a6 --- /dev/null +++ b/nltk/test/corpus.doctest @@ -0,0 +1,2336 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +================ + Corpus Readers +================ + +The `nltk.corpus` package defines a collection of *corpus reader* +classes, which can be used to access the contents of a diverse set of +corpora. The list of available corpora is given at: + +https://www.nltk.org/nltk_data/ + +Each corpus reader class is specialized to handle a specific +corpus format. In addition, the `nltk.corpus` package automatically +creates a set of corpus reader instances that can be used to access +the corpora in the NLTK data package. +Section `Corpus Reader Objects`_ ("Corpus Reader Objects") describes +the corpus reader instances that can be used to read the corpora in +the NLTK data package. Section `Corpus Reader Classes`_ ("Corpus +Reader Classes") describes the corpus reader classes themselves, and +discusses the issues involved in creating new corpus reader objects +and new corpus reader classes. Section `Regression Tests`_ +("Regression Tests") contains regression tests for the corpus readers +and associated functions and classes. + +.. contents:: **Table of Contents** + :depth: 4 + :backlinks: none + +--------------------- +Corpus Reader Objects +--------------------- + +Overview +======== + +NLTK includes a diverse set of corpora which can be +read using the ``nltk.corpus`` package. Each corpus is accessed by +means of a "corpus reader" object from ``nltk.corpus``: + + >>> import nltk.corpus + >>> # The Brown corpus: + >>> print(str(nltk.corpus.brown).replace('\\\\','/')) + + >>> # The Penn Treebank Corpus: + >>> print(str(nltk.corpus.treebank).replace('\\\\','/')) + + >>> # The Name Genders Corpus: + >>> print(str(nltk.corpus.names).replace('\\\\','/')) + + >>> # The Inaugural Address Corpus: + >>> print(str(nltk.corpus.inaugural).replace('\\\\','/')) + + +Most corpora consist of a set of files, each containing a document (or +other pieces of text). A list of identifiers for these files is +accessed via the ``fileids()`` method of the corpus reader: + + >>> nltk.corpus.treebank.fileids() + ['wsj_0001.mrg', 'wsj_0002.mrg', 'wsj_0003.mrg', 'wsj_0004.mrg', ...] + >>> nltk.corpus.inaugural.fileids() + ['1789-Washington.txt', '1793-Washington.txt', '1797-Adams.txt', ...] + +Each corpus reader provides a variety of methods to read data from the +corpus, depending on the format of the corpus. For example, plaintext +corpora support methods to read the corpus as raw text, a list of +words, a list of sentences, or a list of paragraphs. + + >>> from nltk.corpus import inaugural + >>> inaugural.raw('1789-Washington.txt') + 'Fellow-Citizens of the Senate ...' + >>> inaugural.words('1789-Washington.txt') + ['Fellow', '-', 'Citizens', 'of', 'the', ...] + >>> inaugural.sents('1789-Washington.txt') + [['Fellow', '-', 'Citizens'...], ['Among', 'the', 'vicissitudes'...]...] + >>> inaugural.paras('1789-Washington.txt') + [[['Fellow', '-', 'Citizens'...]], + [['Among', 'the', 'vicissitudes'...], + ['On', 'the', 'one', 'hand', ',', 'I'...]...]...] + +Each of these reader methods may be given a single document's item +name or a list of document item names. When given a list of document +item names, the reader methods will concatenate together the contents +of the individual documents. + + >>> l1 = len(inaugural.words('1789-Washington.txt')) + >>> l2 = len(inaugural.words('1793-Washington.txt')) + >>> l3 = len(inaugural.words(['1789-Washington.txt', '1793-Washington.txt'])) + >>> print('%s+%s == %s' % (l1, l2, l3)) + 1538+147 == 1685 + +If the reader methods are called without any arguments, they will +typically load all documents in the corpus. + + >>> len(inaugural.words()) + 152901 + +If a corpus contains a README file, it can be accessed with a ``readme()`` method: + + >>> inaugural.readme()[:32] + 'C-Span Inaugural Address Corpus\n' + +Plaintext Corpora +================= + +Here are the first few words from each of NLTK's plaintext corpora: + + >>> nltk.corpus.abc.words() + ['PM', 'denies', 'knowledge', 'of', 'AWB', ...] + >>> nltk.corpus.genesis.words() + ['In', 'the', 'beginning', 'God', 'created', ...] + >>> nltk.corpus.gutenberg.words(fileids='austen-emma.txt') + ['[', 'Emma', 'by', 'Jane', 'Austen', '1816', ...] + >>> nltk.corpus.inaugural.words() + ['Fellow', '-', 'Citizens', 'of', 'the', ...] + >>> nltk.corpus.state_union.words() + ['PRESIDENT', 'HARRY', 'S', '.', 'TRUMAN', "'", ...] + >>> nltk.corpus.webtext.words() + ['Cookie', 'Manager', ':', '"', 'Don', "'", 't', ...] + +Tagged Corpora +============== + +In addition to the plaintext corpora, NLTK's data package also +contains a wide variety of annotated corpora. For example, the Brown +Corpus is annotated with part-of-speech tags, and defines additional +methods ``tagged_*()`` which words as `(word,tag)` tuples, rather +than just bare word strings. + + >>> from nltk.corpus import brown + >>> print(brown.words()) + ['The', 'Fulton', 'County', 'Grand', 'Jury', ...] + >>> print(brown.tagged_words()) + [('The', 'AT'), ('Fulton', 'NP-TL'), ...] + >>> print(brown.sents()) + [['The', 'Fulton', 'County'...], ['The', 'jury', 'further'...], ...] + >>> print(brown.tagged_sents()) + [[('The', 'AT'), ('Fulton', 'NP-TL')...], + [('The', 'AT'), ('jury', 'NN'), ('further', 'RBR')...]...] + >>> print(brown.paras(categories='reviews')) + [[['It', 'is', 'not', 'news', 'that', 'Nathan', 'Milstein'...], + ['Certainly', 'not', 'in', 'Orchestra', 'Hall', 'where'...]], + [['There', 'was', 'about', 'that', 'song', 'something', ...], + ['Not', 'the', 'noblest', 'performance', 'we', 'have', ...], ...], ...] + >>> print(brown.tagged_paras(categories='reviews')) + [[[('It', 'PPS'), ('is', 'BEZ'), ('not', '*'), ...], + [('Certainly', 'RB'), ('not', '*'), ('in', 'IN'), ...]], + [[('There', 'EX'), ('was', 'BEDZ'), ('about', 'IN'), ...], + [('Not', '*'), ('the', 'AT'), ('noblest', 'JJT'), ...], ...], ...] + +Similarly, the Indian Language POS-Tagged Corpus includes samples of +Indian text annotated with part-of-speech tags: + + >>> from nltk.corpus import indian + >>> print(indian.words()) # doctest: +SKIP + ['\xe0\xa6\xae\xe0\xa6\xb9\xe0\xa6\xbf\...', + '\xe0\xa6\xb8\xe0\xa6\xa8\xe0\xa7\x8d\xe0...', ...] + >>> print(indian.tagged_words()) # doctest: +SKIP + [('\xe0\xa6\xae\xe0\xa6\xb9\xe0\xa6\xbf...', 'NN'), + ('\xe0\xa6\xb8\xe0\xa6\xa8\xe0\xa7\x8d\xe0...', 'NN'), ...] + +Several tagged corpora support access to a simplified, universal tagset, e.g. where all nouns +tags are collapsed to a single category ``NOUN``: + + >>> print(brown.tagged_sents(tagset='universal')) + [[('The', 'DET'), ('Fulton', 'NOUN'), ('County', 'NOUN'), ('Grand', 'ADJ'), ('Jury', 'NOUN'), ...], + [('The', 'DET'), ('jury', 'NOUN'), ('further', 'ADV'), ('said', 'VERB'), ('in', 'ADP'), ...]...] + >>> from nltk.corpus import conll2000, switchboard + >>> print(conll2000.tagged_words(tagset='universal')) + [('Confidence', 'NOUN'), ('in', 'ADP'), ...] + +Use ``nltk.app.pos_concordance()`` to access a GUI for searching tagged corpora. + +Chunked Corpora +=============== + +The CoNLL corpora also provide chunk structures, which are encoded as +flat trees. The CoNLL 2000 Corpus includes phrasal chunks; and the +CoNLL 2002 Corpus includes named entity chunks. + + >>> from nltk.corpus import conll2000, conll2002 + >>> print(conll2000.sents()) + [['Confidence', 'in', 'the', 'pound', 'is', 'widely', ...], + ['Chancellor', 'of', 'the', 'Exchequer', ...], ...] + >>> for tree in conll2000.chunked_sents()[:2]: + ... print(tree) + (S + (NP Confidence/NN) + (PP in/IN) + (NP the/DT pound/NN) + (VP is/VBZ widely/RB expected/VBN to/TO take/VB) + (NP another/DT sharp/JJ dive/NN) + if/IN + ...) + (S + Chancellor/NNP + (PP of/IN) + (NP the/DT Exchequer/NNP) + ...) + >>> print(conll2002.sents()) + [['Sao', 'Paulo', '(', 'Brasil', ')', ',', ...], ['-'], ...] + >>> for tree in conll2002.chunked_sents()[:2]: + ... print(tree) + (S + (LOC Sao/NC Paulo/VMI) + (/Fpa + (LOC Brasil/NC) + )/Fpt + ...) + (S -/Fg) + +.. note:: Since the CONLL corpora do not contain paragraph break + information, these readers do not support the ``para()`` method.) + +.. warning:: if you call the conll corpora reader methods without any + arguments, they will return the contents of the entire corpus, + *including* the 'test' portions of the corpus.) + +SemCor is a subset of the Brown corpus tagged with WordNet senses and +named entities. Both kinds of lexical items include multiword units, +which are encoded as chunks (senses and part-of-speech tags pertain +to the entire chunk). + + >>> from nltk.corpus import semcor + >>> semcor.words() + ['The', 'Fulton', 'County', 'Grand', 'Jury', ...] + >>> semcor.chunks() + [['The'], ['Fulton', 'County', 'Grand', 'Jury'], ...] + >>> semcor.sents() + [['The', 'Fulton', 'County', 'Grand', 'Jury', 'said', ...], + ['The', 'jury', 'further', 'said', ...], ...] + >>> semcor.chunk_sents() + [[['The'], ['Fulton', 'County', 'Grand', 'Jury'], ['said'], ... + ['.']], [['The'], ['jury'], ['further'], ['said'], ... ['.']], ...] + >>> list(map(str, semcor.tagged_chunks(tag='both')[:3])) + ['(DT The)', "(Lemma('group.n.01.group') (NE (NNP Fulton County Grand Jury)))", "(Lemma('state.v.01.say') (VB said))"] + >>> [[str(c) for c in s] for s in semcor.tagged_sents(tag='both')[:2]] + [['(DT The)', "(Lemma('group.n.01.group') (NE (NNP Fulton County Grand Jury)))", ... + '(None .)'], ['(DT The)', ... '(None .)']] + + +The IEER corpus is another chunked corpus. This corpus is unusual in +that each corpus item contains multiple documents. (This reflects the +fact that each corpus file contains multiple documents.) The IEER +corpus defines the `parsed_docs` method, which returns the documents +in a given item as `IEERDocument` objects: + + >>> from nltk.corpus import ieer + >>> ieer.fileids() + ['APW_19980314', 'APW_19980424', 'APW_19980429', + 'NYT_19980315', 'NYT_19980403', 'NYT_19980407'] + >>> docs = ieer.parsed_docs('APW_19980314') + >>> print(docs[0]) + + >>> print(docs[0].docno) + APW19980314.0391 + >>> print(docs[0].doctype) + NEWS STORY + >>> print(docs[0].date_time) + 03/14/1998 10:36:00 + >>> print(docs[0].headline) + (DOCUMENT Kenyans protest tax hikes) + >>> print(docs[0].text) + (DOCUMENT + (LOCATION NAIROBI) + , + (LOCATION Kenya) + ( + (ORGANIZATION AP) + ) + _ + (CARDINAL Thousands) + of + laborers, + ... + on + (DATE Saturday) + ...) + +Parsed Corpora +============== + +The Treebank corpora provide a syntactic parse for each sentence. The +NLTK data package includes a 10% sample of the Penn Treebank (in +``treebank``), as well as the Sinica Treebank (in ``sinica_treebank``). + +Reading the Penn Treebank (Wall Street Journal sample): + + >>> from nltk.corpus import treebank + >>> print(treebank.fileids()) + ['wsj_0001.mrg', 'wsj_0002.mrg', 'wsj_0003.mrg', 'wsj_0004.mrg', ...] + >>> print(treebank.words('wsj_0003.mrg')) + ['A', 'form', 'of', 'asbestos', 'once', 'used', ...] + >>> print(treebank.tagged_words('wsj_0003.mrg')) + [('A', 'DT'), ('form', 'NN'), ('of', 'IN'), ...] + >>> print(treebank.parsed_sents('wsj_0003.mrg')[0]) + (S + (S-TPC-1 + (NP-SBJ + (NP (NP (DT A) (NN form)) (PP (IN of) (NP (NN asbestos)))) + (RRC ...)...)...) + ... + (VP (VBD reported) (SBAR (-NONE- 0) (S (-NONE- *T*-1)))) + (. .)) + +If you have access to a full installation of the Penn Treebank, NLTK +can be configured to load it as well. Download the ``ptb`` package, +and in the directory ``nltk_data/corpora/ptb`` place the ``BROWN`` +and ``WSJ`` directories of the Treebank installation (symlinks work +as well). Then use the ``ptb`` module instead of ``treebank``: + + >>> from nltk.corpus import ptb + >>> print(ptb.fileids()) # doctest: +SKIP + ['BROWN/CF/CF01.MRG', 'BROWN/CF/CF02.MRG', 'BROWN/CF/CF03.MRG', 'BROWN/CF/CF04.MRG', ...] + >>> print(ptb.words('WSJ/00/WSJ_0003.MRG')) # doctest: +SKIP + ['A', 'form', 'of', 'asbestos', 'once', 'used', '*', ...] + >>> print(ptb.tagged_words('WSJ/00/WSJ_0003.MRG')) # doctest: +SKIP + [('A', 'DT'), ('form', 'NN'), ('of', 'IN'), ...] + +...and so forth, like ``treebank`` but with extended fileids. Categories +specified in ``allcats.txt`` can be used to filter by genre; they consist +of ``news`` (for WSJ articles) and names of the Brown subcategories +(``fiction``, ``humor``, ``romance``, etc.): + + >>> ptb.categories() # doctest: +SKIP + ['adventure', 'belles_lettres', 'fiction', 'humor', 'lore', 'mystery', 'news', 'romance', 'science_fiction'] + >>> print(ptb.fileids('news')) # doctest: +SKIP + ['WSJ/00/WSJ_0001.MRG', 'WSJ/00/WSJ_0002.MRG', 'WSJ/00/WSJ_0003.MRG', ...] + >>> print(ptb.words(categories=['humor','fiction'])) # doctest: +SKIP + ['Thirty-three', 'Scotty', 'did', 'not', 'go', 'back', ...] + +As PropBank and NomBank depend on the (WSJ portion of the) Penn Treebank, +the modules ``propbank_ptb`` and ``nombank_ptb`` are provided for access +to a full PTB installation. + +Reading the Sinica Treebank: + + >>> from nltk.corpus import sinica_treebank + >>> print(sinica_treebank.sents()) # doctest: +SKIP + [['\xe4\xb8\x80'], ['\xe5\x8f\x8b\xe6\x83\x85'], ...] + >>> sinica_treebank.parsed_sents()[25] # doctest: +SKIP + Tree('S', + [Tree('NP', + [Tree('Nba', ['\xe5\x98\x89\xe7\x8f\x8d'])]), + Tree('V\xe2\x80\xa7\xe5\x9c\xb0', + [Tree('VA11', ['\xe4\xb8\x8d\xe5\x81\x9c']), + Tree('DE', ['\xe7\x9a\x84'])]), + Tree('VA4', ['\xe5\x93\xad\xe6\xb3\xa3'])]) + +Reading the CoNLL 2007 Dependency Treebanks: + + >>> from nltk.corpus import conll2007 + >>> conll2007.sents('esp.train')[0] # doctest: +SKIP + ['El', 'aumento', 'del', 'índice', 'de', 'desempleo', ...] + >>> conll2007.parsed_sents('esp.train')[0] # doctest: +SKIP + + >>> print(conll2007.parsed_sents('esp.train')[0].tree()) # doctest: +SKIP + (fortaleció + (aumento El (del (índice (de (desempleo estadounidense))))) + hoy + considerablemente + (al + (euro + (cotizaba + , + que + (a (15.35 las GMT)) + se + (en (mercado el (de divisas) (de Fráncfort))) + (a 0,9452_dólares) + (frente_a , (0,9349_dólares los (de (mañana esta))))))) + .) + +Word Lists and Lexicons +======================= + +The NLTK data package also includes a number of lexicons and word +lists. These are accessed just like text corpora. The following +examples illustrate the use of the wordlist corpora: + + >>> from nltk.corpus import names, stopwords, words + >>> words.fileids() + ['en', 'en-basic'] + >>> words.words('en') + ['A', 'a', 'aa', 'aal', 'aalii', 'aam', 'Aani', 'aardvark', 'aardwolf', ...] + + >>> stopwords.fileids() # doctest: +SKIP + ['arabic', 'azerbaijani', 'bengali', 'danish', 'dutch', 'english', 'finnish', 'french', ...] + >>> sorted(stopwords.words('portuguese')) + ['a', 'ao', 'aos', 'aquela', 'aquelas', 'aquele', 'aqueles', ...] + >>> names.fileids() + ['female.txt', 'male.txt'] + >>> names.words('male.txt') + ['Aamir', 'Aaron', 'Abbey', 'Abbie', 'Abbot', 'Abbott', ...] + >>> names.words('female.txt') + ['Abagael', 'Abagail', 'Abbe', 'Abbey', 'Abbi', 'Abbie', ...] + +The CMU Pronunciation Dictionary corpus contains pronunciation +transcriptions for over 100,000 words. It can be accessed as a list +of entries (where each entry consists of a word, an identifier, and a +transcription) or as a dictionary from words to lists of +transcriptions. Transcriptions are encoded as tuples of phoneme +strings. + + >>> from nltk.corpus import cmudict + >>> print(cmudict.entries()[653:659]) + [('acetate', ['AE1', 'S', 'AH0', 'T', 'EY2', 'T']), + ('acetic', ['AH0', 'S', 'EH1', 'T', 'IH0', 'K']), + ('acetic', ['AH0', 'S', 'IY1', 'T', 'IH0', 'K']), + ('aceto', ['AA0', 'S', 'EH1', 'T', 'OW0']), + ('acetochlor', ['AA0', 'S', 'EH1', 'T', 'OW0', 'K', 'L', 'AO2', 'R']), + ('acetone', ['AE1', 'S', 'AH0', 'T', 'OW2', 'N'])] + >>> # Load the entire cmudict corpus into a Python dictionary: + >>> transcr = cmudict.dict() + >>> print([transcr[w][0] for w in 'Natural Language Tool Kit'.lower().split()]) + [['N', 'AE1', 'CH', 'ER0', 'AH0', 'L'], + ['L', 'AE1', 'NG', 'G', 'W', 'AH0', 'JH'], + ['T', 'UW1', 'L'], + ['K', 'IH1', 'T']] + + +WordNet +======= + +Please see the separate WordNet howto. + +FrameNet +======== + +Please see the separate FrameNet howto. + +PropBank +======== + +Please see the separate PropBank howto. + +SentiWordNet +============ + +Please see the separate SentiWordNet howto. + +Categorized Corpora +=================== + +Several corpora included with NLTK contain documents that have been categorized for +topic, genre, polarity, etc. In addition to the standard corpus interface, these +corpora provide access to the list of categories and the mapping between the documents +and their categories (in both directions). Access the categories using the ``categories()`` +method, e.g.: + + >>> from nltk.corpus import brown, movie_reviews, reuters + >>> brown.categories() + ['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', + 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction'] + >>> movie_reviews.categories() + ['neg', 'pos'] + >>> reuters.categories() + ['acq', 'alum', 'barley', 'bop', 'carcass', 'castor-oil', 'cocoa', + 'coconut', 'coconut-oil', 'coffee', 'copper', 'copra-cake', 'corn', + 'cotton', 'cotton-oil', 'cpi', 'cpu', 'crude', 'dfl', 'dlr', ...] + +This method has an optional argument that specifies a document or a list +of documents, allowing us to map from (one or more) documents to (one or more) categories: + + >>> brown.categories('ca01') + ['news'] + >>> brown.categories(['ca01','cb01']) + ['editorial', 'news'] + >>> reuters.categories('training/9865') + ['barley', 'corn', 'grain', 'wheat'] + >>> reuters.categories(['training/9865', 'training/9880']) + ['barley', 'corn', 'grain', 'money-fx', 'wheat'] + +We can go back the other way using the optional argument of the ``fileids()`` method: + + >>> reuters.fileids('barley') + ['test/15618', 'test/15649', 'test/15676', 'test/15728', 'test/15871', ...] + +Both the ``categories()`` and ``fileids()`` methods return a sorted list containing +no duplicates. + +In addition to mapping between categories and documents, these corpora permit +direct access to their contents via the categories. Instead of accessing a subset +of a corpus by specifying one or more fileids, we can identify one or more categories, e.g.: + + >>> brown.tagged_words(categories='news') + [('The', 'AT'), ('Fulton', 'NP-TL'), ...] + >>> brown.sents(categories=['editorial','reviews']) + [['Assembly', 'session', 'brought', 'much', 'good'], ['The', 'General', + 'Assembly', ',', 'which', 'adjourns', 'today', ',', 'has', 'performed', + 'in', 'an', 'atmosphere', 'of', 'crisis', 'and', 'struggle', 'from', + 'the', 'day', 'it', 'convened', '.'], ...] + +Note that it is an error to specify both documents and categories. + +In the context of a text categorization system, we can easily test if the +category assigned to a document is correct as follows: + + >>> def classify(doc): return 'news' # Trivial classifier + >>> doc = 'ca01' + >>> classify(doc) in brown.categories(doc) + True + + +Other Corpora +============= + +comparative_sentences +--------------------- +A list of sentences from various sources, especially reviews and articles. Each +line contains one sentence; sentences were separated by using a sentence tokenizer. +Comparative sentences have been annotated with their type, entities, features and +keywords. + + >>> from nltk.corpus import comparative_sentences + >>> comparison = comparative_sentences.comparisons()[0] + >>> comparison.text + ['its', 'fast-forward', 'and', 'rewind', 'work', 'much', 'more', 'smoothly', + 'and', 'consistently', 'than', 'those', 'of', 'other', 'models', 'i', "'ve", + 'had', '.'] + >>> comparison.entity_2 + 'models' + >>> (comparison.feature, comparison.keyword) + ('rewind', 'more') + >>> len(comparative_sentences.comparisons()) + 853 + +opinion_lexicon +--------------- +A list of positive and negative opinion words or sentiment words for English. + + >>> from nltk.corpus import opinion_lexicon + >>> opinion_lexicon.words()[:4] + ['2-faced', '2-faces', 'abnormal', 'abolish'] + +The OpinionLexiconCorpusReader also provides shortcuts to retrieve positive/negative +words: + + >>> opinion_lexicon.negative()[:4] + ['2-faced', '2-faces', 'abnormal', 'abolish'] + +Note that words from `words()` method in opinion_lexicon are sorted by file id, +not alphabetically: + + >>> opinion_lexicon.words()[0:10] + ['2-faced', '2-faces', 'abnormal', 'abolish', 'abominable', 'abominably', + 'abominate', 'abomination', 'abort', 'aborted'] + >>> sorted(opinion_lexicon.words())[0:10] + ['2-faced', '2-faces', 'a+', 'abnormal', 'abolish', 'abominable', 'abominably', + 'abominate', 'abomination', 'abort'] + +ppattach +-------- +The Prepositional Phrase Attachment corpus is a corpus of +prepositional phrase attachment decisions. Each instance in the +corpus is encoded as a ``PPAttachment`` object: + + >>> from nltk.corpus import ppattach + >>> ppattach.attachments('training') + [PPAttachment(sent='0', verb='join', noun1='board', + prep='as', noun2='director', attachment='V'), + PPAttachment(sent='1', verb='is', noun1='chairman', + prep='of', noun2='N.V.', attachment='N'), + ...] + >>> inst = ppattach.attachments('training')[0] + >>> (inst.sent, inst.verb, inst.noun1, inst.prep, inst.noun2) + ('0', 'join', 'board', 'as', 'director') + >>> inst.attachment + 'V' + +product_reviews_1 and product_reviews_2 +--------------------------------------- +These two datasets respectively contain annotated customer reviews of 5 and 9 +products from amazon.com. + + >>> from nltk.corpus import product_reviews_1 + >>> camera_reviews = product_reviews_1.reviews('Canon_G3.txt') + >>> review = camera_reviews[0] + >>> review.sents()[0] + ['i', 'recently', 'purchased', 'the', 'canon', 'powershot', 'g3', 'and', 'am', + 'extremely', 'satisfied', 'with', 'the', 'purchase', '.'] + >>> review.features() + [('canon powershot g3', '+3'), ('use', '+2'), ('picture', '+2'), + ('picture quality', '+1'), ('picture quality', '+1'), ('camera', '+2'), + ('use', '+2'), ('feature', '+1'), ('picture quality', '+3'), ('use', '+1'), + ('option', '+1')] + +It is also possible to reach the same information directly from the stream: + + >>> product_reviews_1.features('Canon_G3.txt') + [('canon powershot g3', '+3'), ('use', '+2'), ...] + +We can compute stats for specific product features: + + >>> n_reviews = len([(feat,score) for (feat,score) in product_reviews_1.features('Canon_G3.txt') if feat=='picture']) + >>> tot = sum([int(score) for (feat,score) in product_reviews_1.features('Canon_G3.txt') if feat=='picture']) + >>> mean = tot / n_reviews + >>> print(n_reviews, tot, mean) + 15 24 1.6 + +pros_cons +--------- +A list of pros/cons sentences for determining context (aspect) dependent +sentiment words, which are then applied to sentiment analysis of comparative +sentences. + + >>> from nltk.corpus import pros_cons + >>> pros_cons.sents(categories='Cons') + [['East', 'batteries', '!', 'On', '-', 'off', 'switch', 'too', 'easy', + 'to', 'maneuver', '.'], ['Eats', '...', 'no', ',', 'GULPS', 'batteries'], + ...] + >>> pros_cons.words('IntegratedPros.txt') + ['Easy', 'to', 'use', ',', 'economical', '!', ...] + +semcor +------ +The Brown Corpus, annotated with WordNet senses. + + >>> from nltk.corpus import semcor + >>> semcor.words('brown2/tagfiles/br-n12.xml') + ['When', 'several', 'minutes', 'had', 'passed', ...] + +senseval +-------- +The Senseval 2 corpus is a word sense disambiguation corpus. Each +item in the corpus corresponds to a single ambiguous word. For each +of these words, the corpus contains a list of instances, corresponding +to occurrences of that word. Each instance provides the word; a list +of word senses that apply to the word occurrence; and the word's +context. + + >>> from nltk.corpus import senseval + >>> senseval.fileids() + ['hard.pos', 'interest.pos', 'line.pos', 'serve.pos'] + >>> senseval.instances('hard.pos') + ... + [SensevalInstance(word='hard-a', + position=20, + context=[('``', '``'), ('he', 'PRP'), ...('hard', 'JJ'), ...], + senses=('HARD1',)), + SensevalInstance(word='hard-a', + position=10, + context=[('clever', 'NNP'), ...('hard', 'JJ'), ('time', 'NN'), ...], + senses=('HARD1',)), ...] + +The following code looks at instances of the word 'interest', and +displays their local context (2 words on each side) and word sense(s): + + >>> for inst in senseval.instances('interest.pos')[:10]: + ... p = inst.position + ... left = ' '.join(w for (w,t) in inst.context[p-2:p]) + ... word = ' '.join(w for (w,t) in inst.context[p:p+1]) + ... right = ' '.join(w for (w,t) in inst.context[p+1:p+3]) + ... senses = ' '.join(inst.senses) + ... print('%20s |%10s | %-15s -> %s' % (left, word, right, senses)) + declines in | interest | rates . -> interest_6 + indicate declining | interest | rates because -> interest_6 + in short-term | interest | rates . -> interest_6 + 4 % | interest | in this -> interest_5 + company with | interests | in the -> interest_5 + , plus | interest | . -> interest_6 + set the | interest | rate on -> interest_6 + 's own | interest | , prompted -> interest_4 + principal and | interest | is the -> interest_6 + increase its | interest | to 70 -> interest_5 + +sentence_polarity +----------------- +The Sentence Polarity dataset contains 5331 positive and 5331 negative processed +sentences. + + >>> from nltk.corpus import sentence_polarity + >>> sentence_polarity.sents() + [['simplistic', ',', 'silly', 'and', 'tedious', '.'], ["it's", 'so', 'laddish', + 'and', 'juvenile', ',', 'only', 'teenage', 'boys', 'could', 'possibly', 'find', + 'it', 'funny', '.'], ...] + >>> sentence_polarity.categories() + ['neg', 'pos'] + >>> sentence_polarity.sents()[1] + ["it's", 'so', 'laddish', 'and', 'juvenile', ',', 'only', 'teenage', 'boys', + 'could', 'possibly', 'find', 'it', 'funny', '.'] + +shakespeare +----------- +The Shakespeare corpus contains a set of Shakespeare plays, formatted +as XML files. These corpora are returned as ElementTree objects: + + >>> from nltk.corpus import shakespeare + >>> from xml.etree import ElementTree + >>> shakespeare.fileids() + ['a_and_c.xml', 'dream.xml', 'hamlet.xml', 'j_caesar.xml', ...] + >>> play = shakespeare.xml('dream.xml') + >>> print(play) + + >>> print('%s: %s' % (play[0].tag, play[0].text)) + TITLE: A Midsummer Night's Dream + >>> personae = [persona.text for persona in + ... play.findall('PERSONAE/PERSONA')] + >>> print(personae) + ['THESEUS, Duke of Athens.', 'EGEUS, father to Hermia.', ...] + >>> # Find and print speakers not listed as personae + >>> names = [persona.split(',')[0] for persona in personae] + >>> speakers = set(speaker.text for speaker in + ... play.findall('*/*/*/SPEAKER')) + >>> print(sorted(speakers.difference(names))) + ['ALL', 'COBWEB', 'DEMETRIUS', 'Fairy', 'HERNIA', 'LYSANDER', + 'Lion', 'MOTH', 'MUSTARDSEED', 'Moonshine', 'PEASEBLOSSOM', + 'Prologue', 'Pyramus', 'Thisbe', 'Wall'] + +subjectivity +------------ +The Subjectivity Dataset contains 5000 subjective and 5000 objective processed +sentences. + + >>> from nltk.corpus import subjectivity + >>> subjectivity.categories() + ['obj', 'subj'] + >>> subjectivity.sents()[23] + ['television', 'made', 'him', 'famous', ',', 'but', 'his', 'biggest', 'hits', + 'happened', 'off', 'screen', '.'] + >>> subjectivity.words(categories='subj') + ['smart', 'and', 'alert', ',', 'thirteen', ...] + +toolbox +------- +The Toolbox corpus distributed with NLTK contains a sample lexicon and +several sample texts from the Rotokas language. The Toolbox corpus +reader returns Toolbox files as XML ElementTree objects. The +following example loads the Rotokas dictionary, and figures out the +distribution of part-of-speech tags for reduplicated words. + +.. doctest: +SKIP + + >>> from nltk.corpus import toolbox + >>> from nltk.probability import FreqDist + >>> from xml.etree import ElementTree + >>> import re + >>> rotokas = toolbox.xml('rotokas.dic') + >>> redup_pos_freqdist = FreqDist() + >>> # Note: we skip over the first record, which is actually + >>> # the header. + >>> for record in rotokas[1:]: + ... lexeme = record.find('lx').text + ... if re.match(r'(.*)\1$', lexeme): + ... redup_pos_freqdist[record.find('ps').text] += 1 + >>> for item, count in redup_pos_freqdist.most_common(): + ... print(item, count) + V 41 + N 14 + ??? 4 + +This example displays some records from a Rotokas text: + +.. doctest: +SKIP + + >>> river = toolbox.xml('rotokas/river.txt', key='ref') + >>> for record in river.findall('record')[:3]: + ... for piece in record: + ... if len(piece.text) > 60: + ... print('%-6s %s...' % (piece.tag, piece.text[:57])) + ... else: + ... print('%-6s %s' % (piece.tag, piece.text)) + ref Paragraph 1 + t ``Viapau oisio ra ovaupasi ... + m viapau oisio ra ovau -pa -si ... + g NEG this way/like this and forget -PROG -2/3.DL... + p NEG ??? CONJ V.I -SUFF.V.3 -SUFF.V... + f ``No ken lus tingting wanema samting papa i bin tok,'' Na... + fe ``Don't forget what Dad said,'' yelled Naomi. + ref 2 + t Osa Ira ora Reviti viapau uvupasiva. + m osa Ira ora Reviti viapau uvu -pa -si ... + g as/like name and name NEG hear/smell -PROG -2/3... + p CONJ N.PN CONJ N.PN NEG V.T -SUFF.V.3 -SUF... + f Tasol Ila na David no bin harim toktok. + fe But Ila and David took no notice. + ref 3 + t Ikaupaoro rokosiva ... + m ikau -pa -oro roko -si -va ... + g run/hurry -PROG -SIM go down -2/3.DL.M -RP ... + p V.T -SUFF.V.3 -SUFF.V.4 ADV -SUFF.V.4 -SUFF.VT.... + f Tupela i bin hariap i go long wara . + fe They raced to the river. + +timit +----- +The NLTK data package includes a fragment of the TIMIT +Acoustic-Phonetic Continuous Speech Corpus. This corpus is broken +down into small speech samples, each of which is available as a wave +file, a phonetic transcription, and a tokenized word list. + + >>> from nltk.corpus import timit + >>> print(timit.utteranceids()) + ['dr1-fvmh0/sa1', 'dr1-fvmh0/sa2', 'dr1-fvmh0/si1466', + 'dr1-fvmh0/si2096', 'dr1-fvmh0/si836', 'dr1-fvmh0/sx116', + 'dr1-fvmh0/sx206', 'dr1-fvmh0/sx26', 'dr1-fvmh0/sx296', ...] + + >>> item = timit.utteranceids()[5] + >>> print(timit.phones(item)) + ['h#', 'k', 'l', 'ae', 's', 'pcl', 'p', 'dh', 'ax', + 's', 'kcl', 'k', 'r', 'ux', 'ix', 'nx', 'y', 'ax', + 'l', 'eh', 'f', 'tcl', 't', 'hh', 'ae', 'n', 'dcl', + 'd', 'h#'] + >>> print(timit.words(item)) + ['clasp', 'the', 'screw', 'in', 'your', 'left', 'hand'] + >>> timit.play(item) # doctest: +SKIP + +The corpus reader can combine the word segmentation information with +the phonemes to produce a single tree structure: + + >>> for tree in timit.phone_trees(item): + ... print(tree) + (S + h# + (clasp k l ae s pcl p) + (the dh ax) + (screw s kcl k r ux) + (in ix nx) + (your y ax) + (left l eh f tcl t) + (hand hh ae n dcl d) + h#) + +The start time and stop time of each phoneme, word, and sentence are +also available: + + >>> print(timit.phone_times(item)) + [('h#', 0, 2190), ('k', 2190, 3430), ('l', 3430, 4326), ...] + >>> print(timit.word_times(item)) + [('clasp', 2190, 8804), ('the', 8804, 9734), ...] + >>> print(timit.sent_times(item)) + [('Clasp the screw in your left hand.', 0, 32154)] + +We can use these times to play selected pieces of a speech sample: + + >>> timit.play(item, 2190, 8804) # 'clasp' # doctest: +SKIP + +The corpus reader can also be queried for information about the +speaker and sentence identifier for a given speech sample: + + >>> print(timit.spkrid(item)) + dr1-fvmh0 + >>> print(timit.sentid(item)) + sx116 + >>> print(timit.spkrinfo(timit.spkrid(item))) + SpeakerInfo(id='VMH0', + sex='F', + dr='1', + use='TRN', + recdate='03/11/86', + birthdate='01/08/60', + ht='5\'05"', + race='WHT', + edu='BS', + comments='BEST NEW ENGLAND ACCENT SO FAR') + + >>> # List the speech samples from the same speaker: + >>> timit.utteranceids(spkrid=timit.spkrid(item)) + ['dr1-fvmh0/sa1', 'dr1-fvmh0/sa2', 'dr1-fvmh0/si1466', ...] + +twitter_samples +--------------- + +Twitter is well-known microblog service that allows public data to be +collected via APIs. NLTK's twitter corpus currently contains a sample of 20k Tweets +retrieved from the Twitter Streaming API. + + >>> from nltk.corpus import twitter_samples + >>> twitter_samples.fileids() + ['negative_tweets.json', 'positive_tweets.json', 'tweets.20150430-223406.json'] + +We follow standard practice in storing full Tweets as line-separated +JSON. These data structures can be accessed via `tweets.docs()`. However, in general it +is more practical to focus just on the text field of the Tweets, which +are accessed via the `strings()` method. + + >>> twitter_samples.strings('tweets.20150430-223406.json')[:5] + ['RT @KirkKus: Indirect cost of the UK being in the EU is estimated to be costing Britain \xa3170 billion per year! #BetterOffOut #UKIP', ...] + +The default tokenizer for Tweets is specialised for 'casual' text, and +the `tokenized()` method returns a list of lists of tokens. + + >>> twitter_samples.tokenized('tweets.20150430-223406.json')[:5] + [['RT', '@KirkKus', ':', 'Indirect', 'cost', 'of', 'the', 'UK', 'being', 'in', ...], + ['VIDEO', ':', 'Sturgeon', 'on', 'post-election', 'deals', 'http://t.co/BTJwrpbmOY'], ...] + +rte +--- +The RTE (Recognizing Textual Entailment) corpus was derived from the +RTE1, RTE2 and RTE3 datasets (dev and test data), and consists of a +list of XML-formatted 'text'/'hypothesis' pairs. + + >>> from nltk.corpus import rte + >>> print(rte.fileids()) + ['rte1_dev.xml', 'rte1_test.xml', 'rte2_dev.xml', ..., 'rte3_test.xml'] + >>> rtepairs = rte.pairs(['rte2_test.xml', 'rte3_test.xml']) + >>> print(rtepairs) + [, , , ...] + +In the gold standard test sets, each pair is labeled according to +whether or not the text 'entails' the hypothesis; the +entailment value is mapped to an integer 1 (True) or 0 (False). + + >>> rtepairs[5] + + >>> rtepairs[5].text + 'His wife Strida won a seat in parliament after forging an alliance + with the main anti-Syrian coalition in the recent election.' + >>> rtepairs[5].hyp + 'Strida elected to parliament.' + >>> rtepairs[5].value + 1 + +The RTE corpus also supports an ``xml()`` method which produces ElementTrees. + + >>> xmltree = rte.xml('rte3_dev.xml') + >>> xmltree # doctest: +SKIP + + >>> xmltree[7].findtext('t') + "Mrs. Bush's approval ratings have remained very high, above 80%, + even as her husband's have recently dropped below 50%." + +verbnet +------- +The VerbNet corpus is a lexicon that divides verbs into classes, based +on their syntax-semantics linking behavior. The basic elements in the +lexicon are verb lemmas, such as 'abandon' and 'accept', and verb +classes, which have identifiers such as 'remove-10.1' and +'admire-31.2-1'. These class identifiers consist of a representative +verb selected from the class, followed by a numerical identifier. The +list of verb lemmas, and the list of class identifiers, can be +retrieved with the following methods: + + >>> from nltk.corpus import verbnet + >>> verbnet.lemmas()[20:25] + ['accelerate', 'accept', 'acclaim', 'accompany', 'accrue'] + >>> verbnet.classids()[:5] + ['accompany-51.7', 'admire-31.2', 'admire-31.2-1', 'admit-65', 'adopt-93'] + +The `classids()` method may also be used to retrieve the classes that +a given lemma belongs to: + + >>> verbnet.classids('accept') + ['approve-77', 'characterize-29.2-1-1', 'obtain-13.5.2'] + +The `classids()` method may additionally be used to retrieve all classes +within verbnet if nothing is passed: + + >>> verbnet.classids() + ['accompany-51.7', 'admire-31.2', 'admire-31.2-1', 'admit-65', 'adopt-93', 'advise-37.9', 'advise-37.9-1', 'allow-64', 'amalgamate-22.2', 'amalgamate-22.2-1', 'amalgamate-22.2-1-1', 'amalgamate-22.2-2', 'amalgamate-22.2-2-1', 'amalgamate-22.2-3', 'amalgamate-22.2-3-1', 'amalgamate-22.2-3-1-1', 'amalgamate-22.2-3-2', 'amuse-31.1', 'animal_sounds-38', 'appeal-31.4', 'appeal-31.4-1', 'appeal-31.4-2', 'appeal-31.4-3', 'appear-48.1.1', 'appoint-29.1', 'approve-77', 'assessment-34', 'assuming_position-50', 'avoid-52', 'banish-10.2', 'battle-36.4', 'battle-36.4-1', 'begin-55.1', 'begin-55.1-1', 'being_dressed-41.3.3', 'bend-45.2', 'berry-13.7', 'bill-54.5', 'body_internal_motion-49', 'body_internal_states-40.6', 'braid-41.2.2', 'break-45.1', 'breathe-40.1.2', 'breathe-40.1.2-1', 'bring-11.3', 'bring-11.3-1', 'build-26.1', 'build-26.1-1', 'bulge-47.5.3', 'bump-18.4', 'bump-18.4-1', 'butter-9.9', 'calibratable_cos-45.6', 'calibratable_cos-45.6-1', 'calve-28', 'captain-29.8', 'captain-29.8-1', 'captain-29.8-1-1', 'care-88', 'care-88-1', 'carry-11.4', 'carry-11.4-1', 'carry-11.4-1-1', 'carve-21.2', 'carve-21.2-1', 'carve-21.2-2', 'change_bodily_state-40.8.4', 'characterize-29.2', 'characterize-29.2-1', 'characterize-29.2-1-1', 'characterize-29.2-1-2', 'chase-51.6', 'cheat-10.6', 'cheat-10.6-1', 'cheat-10.6-1-1', 'chew-39.2', 'chew-39.2-1', 'chew-39.2-2', 'chit_chat-37.6', 'clear-10.3', 'clear-10.3-1', 'cling-22.5', 'coil-9.6', 'coil-9.6-1', 'coloring-24', 'complain-37.8', 'complete-55.2', 'concealment-16', 'concealment-16-1', 'confess-37.10', 'confine-92', 'confine-92-1', 'conjecture-29.5', 'conjecture-29.5-1', 'conjecture-29.5-2', 'consider-29.9', 'consider-29.9-1', 'consider-29.9-1-1', 'consider-29.9-1-1-1', 'consider-29.9-2', 'conspire-71', 'consume-66', 'consume-66-1', 'contiguous_location-47.8', 'contiguous_location-47.8-1', 'contiguous_location-47.8-2', 'continue-55.3', 'contribute-13.2', 'contribute-13.2-1', 'contribute-13.2-1-1', 'contribute-13.2-1-1-1', 'contribute-13.2-2', 'contribute-13.2-2-1', 'convert-26.6.2', 'convert-26.6.2-1', 'cooking-45.3', 'cooperate-73', 'cooperate-73-1', 'cooperate-73-2', 'cooperate-73-3', 'cope-83', 'cope-83-1', 'cope-83-1-1', 'correlate-86', 'correspond-36.1', 'correspond-36.1-1', 'correspond-36.1-1-1', 'cost-54.2', 'crane-40.3.2', 'create-26.4', 'create-26.4-1', 'curtsey-40.3.3', 'cut-21.1', 'cut-21.1-1', 'debone-10.8', 'declare-29.4', 'declare-29.4-1', 'declare-29.4-1-1', 'declare-29.4-1-1-1', 'declare-29.4-1-1-2', 'declare-29.4-1-1-3', 'declare-29.4-2', 'dedicate-79', 'defend-85', 'destroy-44', 'devour-39.4', 'devour-39.4-1', 'devour-39.4-2', 'differ-23.4', 'dine-39.5', 'disappearance-48.2', 'disassemble-23.3', 'discover-84', 'discover-84-1', 'discover-84-1-1', 'dress-41.1.1', 'dressing_well-41.3.2', 'drive-11.5', 'drive-11.5-1', 'dub-29.3', 'dub-29.3-1', 'eat-39.1', 'eat-39.1-1', 'eat-39.1-2', 'enforce-63', 'engender-27', 'entity_specific_cos-45.5', 'entity_specific_modes_being-47.2', 'equip-13.4.2', 'equip-13.4.2-1', 'equip-13.4.2-1-1', 'escape-51.1', 'escape-51.1-1', 'escape-51.1-2', 'escape-51.1-2-1', 'exceed-90', 'exchange-13.6', 'exchange-13.6-1', 'exchange-13.6-1-1', 'exhale-40.1.3', 'exhale-40.1.3-1', 'exhale-40.1.3-2', 'exist-47.1', 'exist-47.1-1', 'exist-47.1-1-1', 'feeding-39.7', 'ferret-35.6', 'fill-9.8', 'fill-9.8-1', 'fit-54.3', 'flinch-40.5', 'floss-41.2.1', 'focus-87', 'forbid-67', 'force-59', 'force-59-1', 'free-80', 'free-80-1', 'fulfilling-13.4.1', 'fulfilling-13.4.1-1', 'fulfilling-13.4.1-2', 'funnel-9.3', 'funnel-9.3-1', 'funnel-9.3-2', 'funnel-9.3-2-1', 'future_having-13.3', 'get-13.5.1', 'get-13.5.1-1', 'give-13.1', 'give-13.1-1', 'gobble-39.3', 'gobble-39.3-1', 'gobble-39.3-2', 'gorge-39.6', 'groom-41.1.2', 'grow-26.2', 'help-72', 'help-72-1', 'herd-47.5.2', 'hiccup-40.1.1', 'hit-18.1', 'hit-18.1-1', 'hold-15.1', 'hold-15.1-1', 'hunt-35.1', 'hurt-40.8.3', 'hurt-40.8.3-1', 'hurt-40.8.3-1-1', 'hurt-40.8.3-2', 'illustrate-25.3', 'image_impression-25.1', 'indicate-78', 'indicate-78-1', 'indicate-78-1-1', 'inquire-37.1.2', 'instr_communication-37.4', 'investigate-35.4', 'judgement-33', 'keep-15.2', 'knead-26.5', 'learn-14', 'learn-14-1', 'learn-14-2', 'learn-14-2-1', 'leave-51.2', 'leave-51.2-1', 'lecture-37.11', 'lecture-37.11-1', 'lecture-37.11-1-1', 'lecture-37.11-2', 'light_emission-43.1', 'limit-76', 'linger-53.1', 'linger-53.1-1', 'lodge-46', 'long-32.2', 'long-32.2-1', 'long-32.2-2', 'manner_speaking-37.3', 'marry-36.2', 'marvel-31.3', 'marvel-31.3-1', 'marvel-31.3-2', 'marvel-31.3-3', 'marvel-31.3-4', 'marvel-31.3-5', 'marvel-31.3-6', 'marvel-31.3-7', 'marvel-31.3-8', 'marvel-31.3-9', 'masquerade-29.6', 'masquerade-29.6-1', 'masquerade-29.6-2', 'matter-91', 'meander-47.7', 'meet-36.3', 'meet-36.3-1', 'meet-36.3-2', 'mine-10.9', 'mix-22.1', 'mix-22.1-1', 'mix-22.1-1-1', 'mix-22.1-2', 'mix-22.1-2-1', 'modes_of_being_with_motion-47.3', 'murder-42.1', 'murder-42.1-1', 'neglect-75', 'neglect-75-1', 'neglect-75-1-1', 'neglect-75-2', 'nonvehicle-51.4.2', 'nonverbal_expression-40.2', 'obtain-13.5.2', 'obtain-13.5.2-1', 'occurrence-48.3', 'order-60', 'order-60-1', 'orphan-29.7', 'other_cos-45.4', 'pain-40.8.1', 'pay-68', 'peer-30.3', 'pelt-17.2', 'performance-26.7', 'performance-26.7-1', 'performance-26.7-1-1', 'performance-26.7-2', 'performance-26.7-2-1', 'pit-10.7', 'pocket-9.10', 'pocket-9.10-1', 'poison-42.2', 'poke-19', 'pour-9.5', 'preparing-26.3', 'preparing-26.3-1', 'preparing-26.3-2', 'price-54.4', 'push-12', 'push-12-1', 'push-12-1-1', 'put-9.1', 'put-9.1-1', 'put-9.1-2', 'put_direction-9.4', 'put_spatial-9.2', 'put_spatial-9.2-1', 'reach-51.8', 'reflexive_appearance-48.1.2', 'refrain-69', 'register-54.1', 'rely-70', 'remove-10.1', 'risk-94', 'risk-94-1', 'roll-51.3.1', 'rummage-35.5', 'run-51.3.2', 'rush-53.2', 'say-37.7', 'say-37.7-1', 'say-37.7-1-1', 'say-37.7-2', 'scribble-25.2', 'search-35.2', 'see-30.1', 'see-30.1-1', 'see-30.1-1-1', 'send-11.1', 'send-11.1-1', 'separate-23.1', 'separate-23.1-1', 'separate-23.1-2', 'settle-89', 'shake-22.3', 'shake-22.3-1', 'shake-22.3-1-1', 'shake-22.3-2', 'shake-22.3-2-1', 'sight-30.2', 'simple_dressing-41.3.1', 'slide-11.2', 'slide-11.2-1-1', 'smell_emission-43.3', 'snooze-40.4', 'sound_emission-43.2', 'sound_existence-47.4', 'spank-18.3', 'spatial_configuration-47.6', 'split-23.2', 'spray-9.7', 'spray-9.7-1', 'spray-9.7-1-1', 'spray-9.7-2', 'stalk-35.3', 'steal-10.5', 'stimulus_subject-30.4', 'stop-55.4', 'stop-55.4-1', 'substance_emission-43.4', 'succeed-74', 'succeed-74-1', 'succeed-74-1-1', 'succeed-74-2', 'suffocate-40.7', 'suspect-81', 'swarm-47.5.1', 'swarm-47.5.1-1', 'swarm-47.5.1-2', 'swarm-47.5.1-2-1', 'swat-18.2', 'talk-37.5', 'tape-22.4', 'tape-22.4-1', 'tell-37.2', 'throw-17.1', 'throw-17.1-1', 'throw-17.1-1-1', 'tingle-40.8.2', 'touch-20', 'touch-20-1', 'transcribe-25.4', 'transfer_mesg-37.1.1', 'transfer_mesg-37.1.1-1', 'transfer_mesg-37.1.1-1-1', 'try-61', 'turn-26.6.1', 'turn-26.6.1-1', 'urge-58', 'vehicle-51.4.1', 'vehicle-51.4.1-1', 'waltz-51.5', 'want-32.1', 'want-32.1-1', 'want-32.1-1-1', 'weather-57', 'weekend-56', 'wink-40.3.1', 'wink-40.3.1-1', 'wipe_instr-10.4.2', 'wipe_instr-10.4.2-1', 'wipe_manner-10.4.1', 'wipe_manner-10.4.1-1', 'wish-62', 'withdraw-82', 'withdraw-82-1', 'withdraw-82-2', 'withdraw-82-3'] + +The primary object in the lexicon is a class record, which is stored +as an ElementTree xml object. The class record for a given class +identifier is returned by the `vnclass()` method: + + >>> verbnet.vnclass('remove-10.1') + + +The `vnclass()` method also accepts "short" identifiers, such as '10.1': + + >>> verbnet.vnclass('10.1') + + +See the Verbnet documentation, or the Verbnet files, for information +about the structure of this xml. As an example, we can retrieve a +list of thematic roles for a given Verbnet class: + + >>> vn_31_2 = verbnet.vnclass('admire-31.2') + >>> for themrole in vn_31_2.findall('THEMROLES/THEMROLE'): + ... print(themrole.attrib['type'], end=' ') + ... for selrestr in themrole.findall('SELRESTRS/SELRESTR'): + ... print('[%(Value)s%(type)s]' % selrestr.attrib, end=' ') + ... print() + Theme + Experiencer [+animate] + Predicate + +The Verbnet corpus also provides a variety of pretty printing +functions that can be used to display the xml contents in a more +concise form. The simplest such method is `pprint()`: + + >>> print(verbnet.pprint('57')) + weather-57 + Subclasses: (none) + Members: blow clear drizzle fog freeze gust hail howl lightning mist + mizzle pelt pour precipitate rain roar shower sleet snow spit spot + sprinkle storm swelter teem thaw thunder + Thematic roles: + * Theme[+concrete +force] + Frames: + Intransitive (Expletive Subject) + Example: It's raining. + Syntax: LEX[it] LEX[[+be]] VERB + Semantics: + * weather(during(E), Weather_type, ?Theme) + NP (Expletive Subject, Theme Object) + Example: It's raining cats and dogs. + Syntax: LEX[it] LEX[[+be]] VERB NP[Theme] + Semantics: + * weather(during(E), Weather_type, Theme) + PP (Expletive Subject, Theme-PP) + Example: It was pelting with rain. + Syntax: LEX[it[+be]] VERB PREP[with] NP[Theme] + Semantics: + * weather(during(E), Weather_type, Theme) + +Verbnet gives us frames that link the syntax and semantics using an example. +These frames are part of the corpus and we can use `frames()` to get a frame +for a given verbnet class. + + >>> frame = verbnet.frames('57') + >>> frame == [{'example': "It's raining.", 'description': {'primary': 'Intransitive', 'secondary': 'Expletive Subject'}, 'syntax': [{'pos_tag': 'LEX', 'modifiers': {'value': 'it', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'LEX', 'modifiers': {'value': '[+be]', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'VERB', 'modifiers': {'value': '', 'selrestrs': [], 'synrestrs': []}}], 'semantics': [{'predicate_value': 'weather', 'arguments': [{'type': 'Event', 'value': 'during(E)'}, {'type': 'VerbSpecific', 'value': 'Weather_type'}, {'type': 'ThemRole', 'value': '?Theme'}], 'negated': False}]}, {'example': "It's raining cats and dogs.", 'description': {'primary': 'NP', 'secondary': 'Expletive Subject, Theme Object'}, 'syntax': [{'pos_tag': 'LEX', 'modifiers': {'value': 'it', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'LEX', 'modifiers': {'value': '[+be]', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'VERB', 'modifiers': {'value': '', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'NP', 'modifiers': {'value': 'Theme', 'selrestrs': [], 'synrestrs': []}}], 'semantics': [{'predicate_value': 'weather', 'arguments': [{'type': 'Event', 'value': 'during(E)'}, {'type': 'VerbSpecific', 'value': 'Weather_type'}, {'type': 'ThemRole', 'value': 'Theme'}], 'negated': False}]}, {'example': 'It was pelting with rain.', 'description': {'primary': 'PP', 'secondary': 'Expletive Subject, Theme-PP'}, 'syntax': [{'pos_tag': 'LEX', 'modifiers': {'value': 'it[+be]', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'VERB', 'modifiers': {'value': '', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'PREP', 'modifiers': {'value': 'with', 'selrestrs': [], 'synrestrs': []}}, {'pos_tag': 'NP', 'modifiers': {'value': 'Theme', 'selrestrs': [], 'synrestrs': []}}], 'semantics': [{'predicate_value': 'weather', 'arguments': [{'type': 'Event', 'value': 'during(E)'}, {'type': 'VerbSpecific', 'value': 'Weather_type'}, {'type': 'ThemRole', 'value': 'Theme'}], 'negated': False}]}] + True + +Verbnet corpus lets us access thematic roles individually using `themroles()`. + + >>> themroles = verbnet.themroles('57') + >>> themroles == [{'modifiers': [{'type': 'concrete', 'value': '+'}, {'type': 'force', 'value': '+'}], 'type': 'Theme'}] + True + +Verbnet classes may also have subclasses sharing similar syntactic and semantic properties +while having differences with the superclass. The Verbnet corpus allows us to access these +subclasses using `subclasses()`. + + >>> print(verbnet.subclasses('9.1')) #Testing for 9.1 since '57' does not have subclasses + ['put-9.1-1', 'put-9.1-2'] + + +nps_chat +-------- + +The NPS Chat Corpus, Release 1.0 consists of over 10,000 posts in age-specific +chat rooms, which have been anonymized, POS-tagged and dialogue-act tagged. + + >>> print(nltk.corpus.nps_chat.words()) + ['now', 'im', 'left', 'with', 'this', 'gay', ...] + >>> print(nltk.corpus.nps_chat.tagged_words()) + [('now', 'RB'), ('im', 'PRP'), ('left', 'VBD'), ...] + >>> print(nltk.corpus.nps_chat.tagged_posts()) + [[('now', 'RB'), ('im', 'PRP'), ('left', 'VBD'), ('with', 'IN'), + ('this', 'DT'), ('gay', 'JJ'), ('name', 'NN')], [(':P', 'UH')], ...] + +We can access the XML elements corresponding to individual posts. These elements +have ``class`` and ``user`` attributes that we can access using ``p.attrib['class']`` +and ``p.attrib['user']``. They also have text content, accessed using ``p.text``. + + >>> print(nltk.corpus.nps_chat.xml_posts()) + [, , ...] + >>> posts = nltk.corpus.nps_chat.xml_posts() + >>> sorted(nltk.FreqDist(p.attrib['class'] for p in posts).keys()) + ['Accept', 'Bye', 'Clarify', 'Continuer', 'Emotion', 'Emphasis', + 'Greet', 'Other', 'Reject', 'Statement', 'System', 'nAnswer', + 'whQuestion', 'yAnswer', 'ynQuestion'] + >>> posts[0].text + 'now im left with this gay name' + +In addition to the above methods for accessing tagged text, we can navigate +the XML structure directly, as follows: + + >>> tokens = posts[0].findall('terminals/t') + >>> [t.attrib['pos'] + "/" + t.attrib['word'] for t in tokens] + ['RB/now', 'PRP/im', 'VBD/left', 'IN/with', 'DT/this', 'JJ/gay', 'NN/name'] + +multext_east +------------ + +The Multext-East Corpus consists of POS-tagged versions of George Orwell's book +1984 in 12 languages: English, Czech, Hungarian, Macedonian, Slovenian, Serbian, +Slovak, Romanian, Estonian, Farsi, Bulgarian and Polish. +The corpus can be accessed using the usual methods for tagged corpora. The tagset +can be transformed from the Multext-East specific MSD tags to the Universal tagset +using the "tagset" parameter of all functions returning tagged parts of the corpus. + + >>> print(nltk.corpus.multext_east.words("oana-en.xml")) + ['It', 'was', 'a', 'bright', ...] + >>> print(nltk.corpus.multext_east.tagged_words("oana-en.xml")) + [('It', '#Pp3ns'), ('was', '#Vmis3s'), ('a', '#Di'), ...] + >>> print(nltk.corpus.multext_east.tagged_sents("oana-en.xml", "universal")) + [[('It', 'PRON'), ('was', 'VERB'), ('a', 'DET'), ...] + + + +--------------------- +Corpus Reader Classes +--------------------- + +NLTK's *corpus reader* classes are used to access the contents of a +diverse set of corpora. Each corpus reader class is specialized to +handle a specific corpus format. Examples include the +`PlaintextCorpusReader`, which handles corpora that consist of a set +of unannotated text files, and the `BracketParseCorpusReader`, which +handles corpora that consist of files containing +parenthesis-delineated parse trees. + +Automatically Created Corpus Reader Instances +============================================= + +When the `nltk.corpus` module is imported, it automatically creates a +set of corpus reader instances that can be used to access the corpora +in the NLTK data distribution. Here is a small sample of those +corpus reader instances: + + >>> import nltk + >>> nltk.corpus.brown + + >>> nltk.corpus.treebank + + >>> nltk.corpus.names + + >>> nltk.corpus.genesis + + >>> nltk.corpus.inaugural + + +This sample illustrates that different corpus reader classes are used +to read different corpora; but that the same corpus reader class may +be used for more than one corpus (e.g., ``genesis`` and ``inaugural``). + +Creating New Corpus Reader Instances +==================================== + +Although the `nltk.corpus` module automatically creates corpus reader +instances for the corpora in the NLTK data distribution, you may +sometimes need to create your own corpus reader. In particular, you +would need to create your own corpus reader if you want... + +- To access a corpus that is not included in the NLTK data + distribution. + +- To access a full copy of a corpus for which the NLTK data + distribution only provides a sample. + +- To access a corpus using a customized corpus reader (e.g., with + a customized tokenizer). + +To create a new corpus reader, you will first need to look up the +signature for that corpus reader's constructor. Different corpus +readers have different constructor signatures, but most of the +constructor signatures have the basic form:: + + SomeCorpusReader(root, files, ...options...) + +Where ``root`` is an absolute path to the directory containing the +corpus data files; ``files`` is either a list of file names (relative +to ``root``) or a regexp specifying which files should be included; +and ``options`` are additional reader-specific options. For example, +we can create a customized corpus reader for the genesis corpus that +uses a different sentence tokenizer as follows: + + >>> # Find the directory where the corpus lives. + >>> genesis_dir = nltk.data.find('corpora/genesis') + >>> # Create our custom sentence tokenizer. + >>> my_sent_tokenizer = nltk.RegexpTokenizer('[^.!?]+') + >>> # Create the new corpus reader object. + >>> my_genesis = nltk.corpus.PlaintextCorpusReader( + ... genesis_dir, r'.*\.txt', sent_tokenizer=my_sent_tokenizer) + >>> # Use the new corpus reader object. + >>> print(my_genesis.sents('english-kjv.txt')[0]) + ['In', 'the', 'beginning', 'God', 'created', 'the', 'heaven', + 'and', 'the', 'earth'] + +If you wish to read your own plaintext corpus, which is stored in the +directory '/usr/share/some-corpus', then you can create a corpus +reader for it with:: + + >>> my_corpus = nltk.corpus.PlaintextCorpusReader( + ... '/usr/share/some-corpus', r'.*\.txt') # doctest: +SKIP + +For a complete list of corpus reader subclasses, see the API +documentation for `nltk.corpus.reader`. + +Corpus Types +============ + +Corpora vary widely in the types of content they include. This is +reflected in the fact that the base class `CorpusReader` only defines +a few general-purpose methods for listing and accessing the files that +make up a corpus. It is up to the subclasses to define *data access +methods* that provide access to the information in the corpus. +However, corpus reader subclasses should be consistent in their +definitions of these data access methods wherever possible. + +At a high level, corpora can be divided into three basic types: + +- A *token corpus* contains information about specific occurrences of + language use (or linguistic tokens), such as dialogues or written + texts. Examples of token corpora are collections of written text + and collections of speech. + +- A *type corpus*, or *lexicon*, contains information about a coherent + set of lexical items (or linguistic types). Examples of lexicons + are dictionaries and word lists. + +- A *language description corpus* contains information about a set of + non-lexical linguistic constructs, such as grammar rules. + +However, many individual corpora blur the distinctions between these +types. For example, corpora that are primarily lexicons may include +token data in the form of example sentences; and corpora that are +primarily token corpora may be accompanied by one or more word lists +or other lexical data sets. + +Because corpora vary so widely in their information content, we have +decided that it would not be wise to use separate corpus reader base +classes for different corpus types. Instead, we simply try to make +the corpus readers consistent wherever possible, but let them differ +where the underlying data itself differs. + +Common Corpus Reader Methods +============================ + +As mentioned above, there are only a handful of methods that all +corpus readers are guaranteed to implement. These methods provide +access to the files that contain the corpus data. Every corpus is +assumed to consist of one or more files, all located in a common root +directory (or in subdirectories of that root directory). The absolute +path to the root directory is stored in the ``root`` property: + + >>> import os + >>> str(nltk.corpus.genesis.root).replace(os.path.sep,'/') + '.../nltk_data/corpora/genesis' + +Each file within the corpus is identified by a platform-independent +identifier, which is basically a path string that uses ``/`` as the +path separator. I.e., this identifier can be converted to a relative +path as follows: + + >>> some_corpus_file_id = nltk.corpus.reuters.fileids()[0] + >>> import os.path + >>> os.path.normpath(some_corpus_file_id).replace(os.path.sep,'/') + 'test/14826' + +To get a list of all data files that make up a corpus, use the +``fileids()`` method. In some corpora, these files will not all contain +the same type of data; for example, for the ``nltk.corpus.timit`` +corpus, ``fileids()`` will return a list including text files, word +segmentation files, phonetic transcription files, sound files, and +metadata files. For corpora with diverse file types, the ``fileids()`` +method will often take one or more optional arguments, which can be +used to get a list of the files with a specific file type: + + >>> nltk.corpus.timit.fileids() + ['dr1-fvmh0/sa1.phn', 'dr1-fvmh0/sa1.txt', 'dr1-fvmh0/sa1.wav', ...] + >>> nltk.corpus.timit.fileids('phn') + ['dr1-fvmh0/sa1.phn', 'dr1-fvmh0/sa2.phn', 'dr1-fvmh0/si1466.phn', ...] + +In some corpora, the files are divided into distinct categories. For +these corpora, the ``fileids()`` method takes an optional argument, +which can be used to get a list of the files within a specific category: + + >>> nltk.corpus.brown.fileids('hobbies') + ['ce01', 'ce02', 'ce03', 'ce04', 'ce05', 'ce06', 'ce07', ...] + +The ``abspath()`` method can be used to find the absolute path to a +corpus file, given its file identifier: + + >>> str(nltk.corpus.brown.abspath('ce06')).replace(os.path.sep,'/') + '.../corpora/brown/ce06' + +The ``abspaths()`` method can be used to find the absolute paths for +one corpus file, a list of corpus files, or (if no fileids are specified), +all corpus files. + +This method is mainly useful as a helper method when defining corpus +data access methods, since data access methods can usually be called +with a string argument (to get a view for a specific file), with a +list argument (to get a view for a specific list of files), or with no +argument (to get a view for the whole corpus). + +Data Access Methods +=================== + +Individual corpus reader subclasses typically extend this basic set of +file-access methods with one or more *data access methods*, which provide +easy access to the data contained in the corpus. The signatures for +data access methods often have the basic form:: + + corpus_reader.some_data access(fileids=None, ...options...) + +Where ``fileids`` can be a single file identifier string (to get a view +for a specific file); a list of file identifier strings (to get a view +for a specific list of files); or None (to get a view for the entire +corpus). Some of the common data access methods, and their return +types, are: + + - I{corpus}.words(): list of str + - I{corpus}.sents(): list of (list of str) + - I{corpus}.paras(): list of (list of (list of str)) + - I{corpus}.tagged_words(): list of (str,str) tuple + - I{corpus}.tagged_sents(): list of (list of (str,str)) + - I{corpus}.tagged_paras(): list of (list of (list of (str,str))) + - I{corpus}.chunked_sents(): list of (Tree w/ (str,str) leaves) + - I{corpus}.parsed_sents(): list of (Tree with str leaves) + - I{corpus}.parsed_paras(): list of (list of (Tree with str leaves)) + - I{corpus}.xml(): A single xml ElementTree + - I{corpus}.raw(): str (unprocessed corpus contents) + +For example, the `words()` method is supported by many different +corpora, and returns a flat list of word strings: + + >>> nltk.corpus.brown.words() + ['The', 'Fulton', 'County', 'Grand', 'Jury', ...] + >>> nltk.corpus.treebank.words() + ['Pierre', 'Vinken', ',', '61', 'years', 'old', ...] + >>> nltk.corpus.conll2002.words() + ['Sao', 'Paulo', '(', 'Brasil', ')', ',', '23', ...] + >>> nltk.corpus.genesis.words() + ['In', 'the', 'beginning', 'God', 'created', ...] + +On the other hand, the `tagged_words()` method is only supported by +corpora that include part-of-speech annotations: + + >>> nltk.corpus.brown.tagged_words() + [('The', 'AT'), ('Fulton', 'NP-TL'), ...] + >>> nltk.corpus.treebank.tagged_words() + [('Pierre', 'NNP'), ('Vinken', 'NNP'), ...] + >>> nltk.corpus.conll2002.tagged_words() + [('Sao', 'NC'), ('Paulo', 'VMI'), ('(', 'Fpa'), ...] + >>> nltk.corpus.genesis.tagged_words() + Traceback (most recent call last): + ... + AttributeError: 'PlaintextCorpusReader' object has no attribute 'tagged_words' + +Although most corpus readers use file identifiers to index their +content, some corpora use different identifiers instead. For example, +the data access methods for the ``timit`` corpus uses *utterance +identifiers* to select which corpus items should be returned: + + >>> nltk.corpus.timit.utteranceids() + ['dr1-fvmh0/sa1', 'dr1-fvmh0/sa2', 'dr1-fvmh0/si1466', ...] + >>> nltk.corpus.timit.words('dr1-fvmh0/sa2') + ["don't", 'ask', 'me', 'to', 'carry', 'an', 'oily', 'rag', 'like', 'that'] + +Attempting to call ``timit``\ 's data access methods with a file +identifier will result in an exception: + + >>> nltk.corpus.timit.fileids() + ['dr1-fvmh0/sa1.phn', 'dr1-fvmh0/sa1.txt', 'dr1-fvmh0/sa1.wav', ...] + >>> nltk.corpus.timit.words('dr1-fvmh0/sa1.txt') # doctest: +SKIP + Traceback (most recent call last): + ... + IOError: No such file or directory: '.../dr1-fvmh0/sa1.txt.wrd' + +As another example, the ``propbank`` corpus defines the ``roleset()`` +method, which expects a roleset identifier, not a file identifier: + + >>> roleset = nltk.corpus.propbank.roleset('eat.01') + >>> from xml.etree import ElementTree as ET + >>> print(ET.tostring(roleset).decode('utf8')) + + + ...... + ... + ... + +Stream Backed Corpus Views +========================== +An important feature of NLTK's corpus readers is that many of them +access the underlying data files using "corpus views." A *corpus +view* is an object that acts like a simple data structure (such as a +list), but does not store the data elements in memory; instead, data +elements are read from the underlying data files on an as-needed +basis. + +By only loading items from the file on an as-needed basis, corpus +views maintain both memory efficiency and responsiveness. The memory +efficiency of corpus readers is important because some corpora contain +very large amounts of data, and storing the entire data set in memory +could overwhelm many machines. The responsiveness is important when +experimenting with corpora in interactive sessions and in in-class +demonstrations. + +The most common corpus view is the `StreamBackedCorpusView`, which +acts as a read-only list of tokens. Two additional corpus view +classes, `ConcatenatedCorpusView` and `LazySubsequence`, make it +possible to create concatenations and take slices of +`StreamBackedCorpusView` objects without actually storing the +resulting list-like object's elements in memory. + +In the future, we may add additional corpus views that act like other +basic data structures, such as dictionaries. + +Writing New Corpus Readers +========================== + +In order to add support for new corpus formats, it is necessary to +define new corpus reader classes. For many corpus formats, writing +new corpus readers is relatively straight-forward. In this section, +we'll describe what's involved in creating a new corpus reader. If +you do create a new corpus reader, we encourage you to contribute it +back to the NLTK project. + +Don't Reinvent the Wheel +------------------------ +Before you start writing a new corpus reader, you should check to be +sure that the desired format can't be read using an existing corpus +reader with appropriate constructor arguments. For example, although +the `TaggedCorpusReader` assumes that words and tags are separated by +``/`` characters by default, an alternative tag-separation character +can be specified via the ``sep`` constructor argument. You should +also check whether the new corpus format can be handled by subclassing +an existing corpus reader, and tweaking a few methods or variables. + +Design +------ +If you decide to write a new corpus reader from scratch, then you +should first decide which data access methods you want the reader to +provide, and what their signatures should be. You should look at +existing corpus readers that process corpora with similar data +contents, and try to be consistent with those corpus readers whenever +possible. + +You should also consider what sets of identifiers are appropriate for +the corpus format. Where it's practical, file identifiers should be +used. However, for some corpora, it may make sense to use additional +sets of identifiers. Each set of identifiers should have a distinct +name (e.g., fileids, utteranceids, rolesets); and you should be consistent +in using that name to refer to that identifier. Do not use parameter +names like ``id``, which leave it unclear what type of identifier is +required. + +Once you've decided what data access methods and identifiers are +appropriate for your corpus, you should decide if there are any +customizable parameters that you'd like the corpus reader to handle. +These parameters make it possible to use a single corpus reader to +handle a wider variety of corpora. The ``sep`` argument for +`TaggedCorpusReader`, mentioned above, is an example of a customizable +corpus reader parameter. + +Implementation +-------------- + +Constructor +~~~~~~~~~~~ +If your corpus reader implements any customizable parameters, then +you'll need to override the constructor. Typically, the new +constructor will first call its base class's constructor, and then +store the customizable parameters. For example, the +`ConllChunkCorpusReader`\ 's constructor is defined as follows: + + >>> def __init__(self, root, fileids, chunk_types, encoding='utf8', + ... tagset=None, separator=None): + ... ConllCorpusReader.__init__( + ... self, root, fileids, ('words', 'pos', 'chunk'), + ... chunk_types=chunk_types, encoding=encoding, + ... tagset=tagset, separator=separator) + +If your corpus reader does not implement any customization parameters, +then you can often just inherit the base class's constructor. + +Data Access Methods +~~~~~~~~~~~~~~~~~~~ + +The most common type of data access method takes an argument +identifying which files to access, and returns a view covering those +files. This argument may be a single file identifier string (to get a +view for a specific file); a list of file identifier strings (to get a +view for a specific list of files); or None (to get a view for the +entire corpus). The method's implementation converts this argument to +a list of path names using the `abspaths()` method, which handles all +three value types (string, list, and None): + + >>> print(str(nltk.corpus.brown.abspaths()).replace('\\\\','/')) + [FileSystemPathPointer('.../corpora/brown/ca01'), + FileSystemPathPointer('.../corpora/brown/ca02'), ...] + >>> print(str(nltk.corpus.brown.abspaths('ce06')).replace('\\\\','/')) + [FileSystemPathPointer('.../corpora/brown/ce06')] + >>> print(str(nltk.corpus.brown.abspaths(['ce06', 'ce07'])).replace('\\\\','/')) + [FileSystemPathPointer('.../corpora/brown/ce06'), + FileSystemPathPointer('.../corpora/brown/ce07')] + +An example of this type of method is the `words()` method, defined by +the `PlaintextCorpusReader` as follows: + + >>> def words(self, fileids=None): + ... return concat([self.CorpusView(fileid, self._read_word_block) + ... for fileid in self.abspaths(fileids)]) + +This method first uses `abspaths()` to convert ``fileids`` to a list of +absolute paths. It then creates a corpus view for each file, using +the `PlaintextCorpusReader._read_word_block()` method to read elements +from the data file (see the discussion of corpus views below). +Finally, it combines these corpus views using the +`nltk.corpus.reader.util.concat()` function. + +When writing a corpus reader for a corpus that is never expected to be +very large, it can sometimes be appropriate to read the files +directly, rather than using a corpus view. For example, the +`WordListCorpusView` class defines its `words()` method as follows: + + >>> def words(self, fileids=None): + ... return concat([[w for w in open(fileid).read().split('\n') if w] + ... for fileid in self.abspaths(fileids)]) + +(This is usually more appropriate for lexicons than for token corpora.) + +If the type of data returned by a data access method is one for which +NLTK has a conventional representation (e.g., words, tagged words, and +parse trees), then you should use that representation. Otherwise, you +may find it necessary to define your own representation. For data +structures that are relatively corpus-specific, it's usually best to +define new classes for these elements. For example, the ``propbank`` +corpus defines the `PropbankInstance` class to store the semantic role +labeling instances described by the corpus; and the ``ppattach`` +corpus defines the `PPAttachment` class to store the prepositional +attachment instances described by the corpus. + +Corpus Views +~~~~~~~~~~~~ +.. (Much of the content for this section is taken from the + StreamBackedCorpusView docstring.) + +The heart of a `StreamBackedCorpusView` is its *block reader* +function, which reads zero or more tokens from a stream, and returns +them as a list. A very simple example of a block reader is: + + >>> def simple_block_reader(stream): + ... return stream.readline().split() + +This simple block reader reads a single line at a time, and returns a +single token (consisting of a string) for each whitespace-separated +substring on the line. A `StreamBackedCorpusView` built from this +block reader will act like a read-only list of all the +whitespace-separated tokens in an underlying file. + +When deciding how to define the block reader for a given corpus, +careful consideration should be given to the size of blocks handled by +the block reader. Smaller block sizes will increase the memory +requirements of the corpus view's internal data structures (by 2 +integers per block). On the other hand, larger block sizes may +decrease performance for random access to the corpus. (But note that +larger block sizes will *not* decrease performance for iteration.) + +Internally, the `StreamBackedCorpusView` class maintains a partial +mapping from token index to file position, with one entry per block. +When a token with a given index *i* is requested, the corpus view +constructs it as follows: + +1. First, it searches the toknum/filepos mapping for the token index + closest to (but less than or equal to) *i*. + +2. Then, starting at the file position corresponding to that index, it + reads one block at a time using the block reader until it reaches + the requested token. + +The toknum/filepos mapping is created lazily: it is initially empty, +but every time a new block is read, the block's initial token is added +to the mapping. (Thus, the toknum/filepos map has one entry per +block.) + +You can create your own corpus view in one of two ways: + +1. Call the `StreamBackedCorpusView` constructor, and provide your + block reader function via the ``block_reader`` argument. + +2. Subclass `StreamBackedCorpusView`, and override the + `read_block()` method. + +The first option is usually easier, but the second option can allow +you to write a single `read_block` method whose behavior can be +customized by different parameters to the subclass's constructor. For +an example of this design pattern, see the `TaggedCorpusView` class, +which is used by `TaggedCorpusView`. + +---------------- +Regression Tests +---------------- + +The following helper functions are used to create and then delete +testing corpora that are stored in temporary directories. These +testing corpora are used to make sure the readers work correctly. + + >>> import tempfile, os.path, textwrap + >>> def make_testcorpus(ext='', **fileids): + ... root = tempfile.mkdtemp() + ... for fileid, contents in fileids.items(): + ... fileid += ext + ... f = open(os.path.join(root, fileid), 'w') + ... f.write(textwrap.dedent(contents)) + ... f.close() + ... return root + >>> def del_testcorpus(root): + ... for fileid in os.listdir(root): + ... os.remove(os.path.join(root, fileid)) + ... os.rmdir(root) + +Plaintext Corpus Reader +======================= +The plaintext corpus reader is used to access corpora that consist of +unprocessed plaintext data. It assumes that paragraph breaks are +indicated by blank lines. Sentences and words can be tokenized using +the default tokenizers, or by custom tokenizers specified as +parameters to the constructor. + + >>> root = make_testcorpus(ext='.txt', + ... a="""\ + ... This is the first sentence. Here is another + ... sentence! And here's a third sentence. + ... + ... This is the second paragraph. Tokenization is currently + ... fairly simple, so the period in Mr. gets tokenized. + ... """, + ... b="""This is the second file.""") + + >>> from nltk.corpus.reader.plaintext import PlaintextCorpusReader + +The list of documents can be specified explicitly, or implicitly (using a +regexp). The ``ext`` argument specifies a file extension. + + >>> corpus = PlaintextCorpusReader(root, ['a.txt', 'b.txt']) + >>> corpus.fileids() + ['a.txt', 'b.txt'] + >>> corpus = PlaintextCorpusReader(root, r'.*\.txt') + >>> corpus.fileids() + ['a.txt', 'b.txt'] + +The directory containing the corpus is corpus.root: + + >>> str(corpus.root) == str(root) + True + +We can get a list of words, or the raw string: + + >>> corpus.words() + ['This', 'is', 'the', 'first', 'sentence', '.', ...] + >>> corpus.raw()[:40] + 'This is the first sentence. Here is ano' + +Check that reading individual documents works, and reading all documents at +once works: + + >>> len(corpus.words()), [len(corpus.words(d)) for d in corpus.fileids()] + (46, [40, 6]) + >>> corpus.words('a.txt') + ['This', 'is', 'the', 'first', 'sentence', '.', ...] + >>> corpus.words('b.txt') + ['This', 'is', 'the', 'second', 'file', '.'] + >>> corpus.words()[:4], corpus.words()[-4:] + (['This', 'is', 'the', 'first'], ['the', 'second', 'file', '.']) + +We're done with the test corpus: + + >>> del_testcorpus(root) + +Test the plaintext corpora that come with nltk: + + >>> from nltk.corpus import abc, genesis, inaugural + >>> from nltk.corpus import state_union, webtext + >>> for corpus in (abc, genesis, inaugural, state_union, + ... webtext): + ... print(str(corpus).replace('\\\\','/')) + ... print(' ', repr(corpus.fileids())[:60]) + ... print(' ', repr(corpus.words()[:10])[:60]) + + ['rural.txt', 'science.txt'] + ['PM', 'denies', 'knowledge', 'of', 'AWB', ... + + ['english-kjv.txt', 'english-web.txt', 'finnish.txt', ... + ['In', 'the', 'beginning', 'God', 'created', 'the', ... + + ['1789-Washington.txt', '1793-Washington.txt', ... + ['Fellow', '-', 'Citizens', 'of', 'the', 'Senate', ... + + ['1945-Truman.txt', '1946-Truman.txt', ... + ['PRESIDENT', 'HARRY', 'S', '.', 'TRUMAN', "'", ... + + ['firefox.txt', 'grail.txt', 'overheard.txt', ... + ['Cookie', 'Manager', ':', '"', 'Don', "'", 't', ... + + +Tagged Corpus Reader +==================== +The Tagged Corpus reader can give us words, sentences, and paragraphs, +each tagged or untagged. All of the read methods can take one item +(in which case they return the contents of that file) or a list of +documents (in which case they concatenate the contents of those files). +By default, they apply to all documents in the corpus. + + >>> root = make_testcorpus( + ... a="""\ + ... This/det is/verb the/det first/adj sentence/noun ./punc + ... Here/det is/verb another/adj sentence/noun ./punc + ... Note/verb that/comp you/pron can/verb use/verb \ + ... any/noun tag/noun set/noun + ... + ... This/det is/verb the/det second/adj paragraph/noun ./punc + ... word/n without/adj a/det tag/noun :/: hello ./punc + ... """, + ... b="""\ + ... This/det is/verb the/det second/adj file/noun ./punc + ... """) + + >>> from nltk.corpus.reader.tagged import TaggedCorpusReader + >>> corpus = TaggedCorpusReader(root, list('ab')) + >>> corpus.fileids() + ['a', 'b'] + >>> str(corpus.root) == str(root) + True + >>> corpus.words() + ['This', 'is', 'the', 'first', 'sentence', '.', ...] + >>> corpus.sents() + [['This', 'is', 'the', 'first', ...], ['Here', 'is', 'another'...], ...] + >>> corpus.paras() + [[['This', ...], ['Here', ...], ...], [['This', ...], ...], ...] + >>> corpus.tagged_words() + [('This', 'DET'), ('is', 'VERB'), ('the', 'DET'), ...] + >>> corpus.tagged_sents() + [[('This', 'DET'), ('is', 'VERB'), ...], [('Here', 'DET'), ...], ...] + >>> corpus.tagged_paras() + [[[('This', 'DET'), ...], ...], [[('This', 'DET'), ...], ...], ...] + >>> corpus.raw()[:40] + 'This/det is/verb the/det first/adj sente' + >>> len(corpus.words()), [len(corpus.words(d)) for d in corpus.fileids()] + (38, [32, 6]) + >>> len(corpus.sents()), [len(corpus.sents(d)) for d in corpus.fileids()] + (6, [5, 1]) + >>> len(corpus.paras()), [len(corpus.paras(d)) for d in corpus.fileids()] + (3, [2, 1]) + >>> print(corpus.words('a')) + ['This', 'is', 'the', 'first', 'sentence', '.', ...] + >>> print(corpus.words('b')) + ['This', 'is', 'the', 'second', 'file', '.'] + >>> del_testcorpus(root) + +The Brown Corpus uses the tagged corpus reader: + + >>> from nltk.corpus import brown + >>> brown.fileids() + ['ca01', 'ca02', 'ca03', 'ca04', 'ca05', 'ca06', 'ca07', ...] + >>> brown.categories() + ['adventure', 'belles_lettres', 'editorial', 'fiction', 'government', 'hobbies', 'humor', + 'learned', 'lore', 'mystery', 'news', 'religion', 'reviews', 'romance', 'science_fiction'] + >>> print(repr(brown.root).replace('\\\\','/')) + FileSystemPathPointer('.../corpora/brown') + >>> brown.words() + ['The', 'Fulton', 'County', 'Grand', 'Jury', ...] + >>> brown.sents() + [['The', 'Fulton', 'County', 'Grand', ...], ...] + >>> brown.paras() + [[['The', 'Fulton', 'County', ...]], [['The', 'jury', ...]], ...] + >>> brown.tagged_words() + [('The', 'AT'), ('Fulton', 'NP-TL'), ...] + >>> brown.tagged_sents() + [[('The', 'AT'), ('Fulton', 'NP-TL'), ('County', 'NN-TL'), ...], ...] + >>> brown.tagged_paras() + [[[('The', 'AT'), ...]], [[('The', 'AT'), ...]], ...] + +Categorized Markdown Corpus Reader +================================== + +This corpus reader class provides additional methods to select features +present in markdown documents. + +First, let's make a test corpus: + + >>> root = make_testcorpus(ext='.md', + ... a="""\ + ... # Section One + ... Here's the first sentence of section one. Then the second sentence. + ... + ... First section, second paragraph. Let's add a [link](https://example.com). + ... + ... # Section Two + ... This section is more fun. It contains an ![image](https://example.com/image.png) followed by a list: + ... + ... 1. First list item + ... 2. Second list item + ... """, + ... b="""\ + ... This is the second file. It starts without a section, but then adds one. + ... + ... # Section 1 + ... This section has a sub-section! + ... + ... ## Section 1a + ... And here's a quote: + ... + ... > Carpe diem + ... + ... HTML tags are removed. + ... """) + +Now, import the ``CategorizedMarkdownCorpusReader`` class. + + >>> from nltk.corpus.reader.markdown import CategorizedMarkdownCorpusReader + +Note that this class requires the following Python packages: + +- ``markdown-it-py`` +- ``mdit-py-plugins`` +- ``mdit-plain`` + +The corpus provides usual methods like ``words()``, ``sents()``, +``paras()``, etc. Each of these methods accepts a list of file IDs +which can be a Python list or a comma-separated string. + + >>> corpus = CategorizedMarkdownCorpusReader(root, ['a.md', 'b.md']) + >>> corpus.fileids() + ['a.md', 'b.md'] + >>> corpus.words() + ['Section', 'One', 'Here', "'", 's', 'the', 'first', ...] + >>> corpus.words('b.md') + ['This', 'is', 'the', 'second', 'file', '.', 'It', ...] + >>> corpus.words('a.md, b.md') == corpus.words(['a.md', 'b.md']) + True + +Here are some methods specific to the +``CategorizedMarkdownCorpusReader`` class to retrieve markdown features: + + >>> corpus.links() + [Link(label='link', href='https://example.com', title=None)] + >>> corpus.images() + [Image(label='image', src='https://example.com/image.png', title=None)] + >>> corpus.lists() + [List(is_ordered=True, items=['First list item', 'Second list item'])] + >>> corpus.blockquotes() + [MarkdownBlock(content='Carpe diem')] + +The corpus can also be broken down into sections based on markdown headings: + + >>> corpus.sections('a.md') + [MarkdownSection(content='Section One\n\nHer...'), MarkdownSection(content='Section Two\n\nThi...')] + >>> for s in corpus.sections(): + ... print(F"{s.heading} (level {s.level})") + ... + Section One (level 1) + Section Two (level 1) + Section 1 (level 1) + Section 1a (level 2) + +Categories +---------- + +The ``CategorizedMarkdownCorpusReader`` relies on YAML front matter to +read metadata defined in markdown documents. This metadata is optional, +and may define one or more categories for each document. + +Let's create another test corpus, this time with some metadata: + + >>> del_testcorpus(root) + >>> root = make_testcorpus(ext='.md', + ... a="""\ + ... --- + ... tags: + ... - tag1 + ... - tag2 + ... --- + ... Document A: category metadata. + ... """, + ... b="""\ + ... --- + ... author: NLTK + ... tags: + ... - tag2 + ... - tag3 + ... --- + ... Document B: additional metadata. + ... """, + ... c="""\ + ... Document C: no metadata. + ... """) + +Load the new corpus and see the ``metadata()`` and ``categories()`` +methods in action: + + >>> fileids = ['a.md', 'b.md', 'c.md'] + >>> corpus = CategorizedMarkdownCorpusReader(root, fileids) + >>> corpus.metadata() + [{'tags': ['tag1', 'tag2']}, {'author': 'NLTK', 'tags': ['tag2', 'tag3']}] + >>> for fid in fileids: + ... print(fid, corpus.metadata(fid)) + ... + a.md [{'tags': ['tag1', 'tag2']}] + b.md [{'author': 'NLTK', 'tags': ['tag2', 'tag3']}] + c.md [] + >>> corpus.categories() + ['tag1', 'tag2', 'tag3'] + >>> corpus.categories('a.md') + ['tag1', 'tag2'] + +The ``fileids()`` method also accepts categories and returns all file +IDs that match any of the specified categories: + + >>> corpus.fileids('tag2') + ['a.md', 'b.md'] + >>> del_testcorpus(root) + +Verbnet Corpus Reader +===================== + +Make sure we're picking up the right number of elements: + + >>> from nltk.corpus import verbnet + >>> len(verbnet.lemmas()) + 3621 + >>> len(verbnet.wordnetids()) + 4953 + >>> len(verbnet.classids()) + 429 + +Selecting classids based on various selectors: + + >>> verbnet.classids(lemma='take') + ['bring-11.3', 'characterize-29.2', 'convert-26.6.2', 'cost-54.2', + 'fit-54.3', 'performance-26.7-2', 'steal-10.5'] + >>> verbnet.classids(wordnetid='lead%2:38:01') + ['accompany-51.7'] + >>> verbnet.classids(fileid='approve-77.xml') + ['approve-77'] + >>> verbnet.classids(classid='admire-31.2') # subclasses + ['admire-31.2-1'] + +vnclass() accepts filenames, long ids, and short ids: + + >>> a = ElementTree.tostring(verbnet.vnclass('admire-31.2.xml')) + >>> b = ElementTree.tostring(verbnet.vnclass('admire-31.2')) + >>> c = ElementTree.tostring(verbnet.vnclass('31.2')) + >>> a == b == c + True + +fileids() can be used to get files based on verbnet class ids: + + >>> verbnet.fileids('admire-31.2') + ['admire-31.2.xml'] + >>> verbnet.fileids(['admire-31.2', 'obtain-13.5.2']) + ['admire-31.2.xml', 'obtain-13.5.2.xml'] + >>> verbnet.fileids('badidentifier') + Traceback (most recent call last): + . . . + ValueError: vnclass identifier 'badidentifier' not found + +longid() and shortid() can be used to convert identifiers: + + >>> verbnet.longid('31.2') + 'admire-31.2' + >>> verbnet.longid('admire-31.2') + 'admire-31.2' + >>> verbnet.shortid('31.2') + '31.2' + >>> verbnet.shortid('admire-31.2') + '31.2' + >>> verbnet.longid('badidentifier') + Traceback (most recent call last): + . . . + ValueError: vnclass identifier 'badidentifier' not found + >>> verbnet.shortid('badidentifier') + Traceback (most recent call last): + . . . + ValueError: vnclass identifier 'badidentifier' not found + +Corpus View Regression Tests +============================ + +Select some corpus files to play with: + + >>> import nltk.data + >>> # A very short file (160 chars): + >>> f1 = nltk.data.find('corpora/inaugural/README') + >>> # A relatively short file (791 chars): + >>> f2 = nltk.data.find('corpora/inaugural/1793-Washington.txt') + >>> # A longer file (32k chars): + >>> f3 = nltk.data.find('corpora/inaugural/1909-Taft.txt') + >>> fileids = [f1, f2, f3] + + +Concatenation +------------- +Check that concatenation works as intended. + + >>> from nltk.corpus.reader.util import * + + >>> c1 = StreamBackedCorpusView(f1, read_whitespace_block, encoding='utf-8') + >>> c2 = StreamBackedCorpusView(f2, read_whitespace_block, encoding='utf-8') + >>> c3 = StreamBackedCorpusView(f3, read_whitespace_block, encoding='utf-8') + >>> c123 = c1+c2+c3 + >>> print(c123) + ['C-Span', 'Inaugural', 'Address', 'Corpus', 'US', ...] + + >>> l1 = f1.open(encoding='utf-8').read().split() + >>> l2 = f2.open(encoding='utf-8').read().split() + >>> l3 = f3.open(encoding='utf-8').read().split() + >>> l123 = l1+l2+l3 + + >>> list(c123) == l123 + True + + >>> (c1+c2+c3)[100] == l123[100] + True + +Slicing +------- +First, do some tests with fairly small slices. These will all +generate tuple values. + + >>> from nltk.util import LazySubsequence + >>> c1 = StreamBackedCorpusView(f1, read_whitespace_block, encoding='utf-8') + >>> l1 = f1.open(encoding='utf-8').read().split() + >>> print(len(c1)) + 21 + >>> len(c1) < LazySubsequence.MIN_SIZE + True + +Choose a list of indices, based on the length, that covers the +important corner cases: + + >>> indices = [-60, -30, -22, -21, -20, -1, + ... 0, 1, 10, 20, 21, 22, 30, 60] + +Test slicing with explicit start & stop value: + + >>> for s in indices: + ... for e in indices: + ... assert list(c1[s:e]) == l1[s:e] + +Test slicing with stop=None: + + >>> for s in indices: + ... assert list(c1[s:]) == l1[s:] + +Test slicing with start=None: + + >>> for e in indices: + ... assert list(c1[:e]) == l1[:e] + +Test slicing with start=stop=None: + + >>> list(c1[:]) == list(l1[:]) + True + +Next, we'll do some tests with much longer slices. These will +generate LazySubsequence objects. + + >>> c3 = StreamBackedCorpusView(f3, read_whitespace_block, encoding='utf-8') + >>> l3 = f3.open(encoding='utf-8').read().split() + >>> print(len(c3)) + 5430 + >>> len(c3) > LazySubsequence.MIN_SIZE*2 + True + +Choose a list of indices, based on the length, that covers the +important corner cases: + + >>> indices = [-12000, -6000, -5431, -5430, -5429, -3000, -200, -1, + ... 0, 1, 200, 3000, 5000, 5429, 5430, 5431, 6000, 12000] + +Test slicing with explicit start & stop value: + + >>> for s in indices: + ... for e in indices: + ... assert list(c3[s:e]) == l3[s:e] + +Test slicing with stop=None: + + >>> for s in indices: + ... assert list(c3[s:]) == l3[s:] + +Test slicing with start=None: + + >>> for e in indices: + ... assert list(c3[:e]) == l3[:e] + +Test slicing with start=stop=None: + + >>> list(c3[:]) == list(l3[:]) + True + +Multiple Iterators +------------------ +If multiple iterators are created for the same corpus view, their +iteration can be interleaved: + + >>> c3 = StreamBackedCorpusView(f3, read_whitespace_block) + >>> iterators = [c3.iterate_from(n) for n in [0,15,30,45]] + >>> for i in range(15): + ... for iterator in iterators: + ... print('%-15s' % next(iterator), end=' ') + ... print() + My a duties in + fellow heavy of a + citizens: weight the proper + Anyone of office sense + who responsibility. upon of + has If which the + taken not, he obligation + the he is which + oath has about the + I no to oath + have conception enter, imposes. + just of or The + taken the he office + must powers is of + feel and lacking an + +SeekableUnicodeStreamReader +=========================== + +The file-like objects provided by the ``codecs`` module unfortunately +suffer from a bug that prevents them from working correctly with +corpus view objects. In particular, although the expose ``seek()`` +and ``tell()`` methods, those methods do not exhibit the expected +behavior, because they are not synchronized with the internal buffers +that are kept by the file-like objects. For example, the ``tell()`` +method will return the file position at the end of the buffers (whose +contents have not yet been returned by the stream); and therefore this +file position can not be used to return to the 'current' location in +the stream (since ``seek()`` has no way to reconstruct the buffers). + +To get around these problems, we define a new class, +`SeekableUnicodeStreamReader`, to act as a file-like interface to +files containing encoded unicode data. This class is loosely based on +the ``codecs.StreamReader`` class. To construct a new reader, we call +the constructor with an underlying stream and an encoding name: + + >>> from io import StringIO, BytesIO + >>> from nltk.data import SeekableUnicodeStreamReader + >>> stream = BytesIO(b"""\ + ... This is a test file. + ... It is encoded in ascii. + ... """.decode('ascii').encode('ascii')) + >>> reader = SeekableUnicodeStreamReader(stream, 'ascii') + +`SeekableUnicodeStreamReader`\ s support all of the normal operations +supplied by a read-only stream. Note that all of the read operations +return ``unicode`` objects (not ``str`` objects). + + >>> reader.read() # read the entire file. + 'This is a test file.\nIt is encoded in ascii.\n' + >>> reader.seek(0) # rewind to the start. + >>> reader.read(5) # read at most 5 bytes. + 'This ' + >>> reader.readline() # read to the end of the line. + 'is a test file.\n' + >>> reader.seek(0) # rewind to the start. + >>> for line in reader: + ... print(repr(line)) # iterate over lines + 'This is a test file.\n' + 'It is encoded in ascii.\n' + >>> reader.seek(0) # rewind to the start. + >>> reader.readlines() # read a list of line strings + ['This is a test file.\n', 'It is encoded in ascii.\n'] + >>> reader.close() + +Size argument to ``read()`` +--------------------------- +The ``size`` argument to ``read()`` specifies the maximum number of +*bytes* to read, not the maximum number of *characters*. Thus, for +encodings that use multiple bytes per character, it may return fewer +characters than the ``size`` argument: + + >>> stream = BytesIO(b"""\ + ... This is a test file. + ... It is encoded in utf-16. + ... """.decode('ascii').encode('utf-16')) + >>> reader = SeekableUnicodeStreamReader(stream, 'utf-16') + >>> reader.read(10) + 'This ' + +If a read block ends in the middle of the byte string encoding a +single character, then that byte string is stored in an internal +buffer, and re-used on the next call to ``read()``. However, if the +size argument is too small to read even a single character, even +though at least one character is available, then the ``read()`` method +will read additional bytes until it can return a single character. +This ensures that the ``read()`` method does not return an empty +string, which could be mistaken for indicating the end of the file. + + >>> reader.seek(0) # rewind to the start. + >>> reader.read(1) # we actually need to read 4 bytes + 'T' + >>> int(reader.tell()) + 4 + +The ``readline()`` method may read more than a single line of text, in +which case it stores the text that it does not return in a buffer. If +this buffer is not empty, then its contents will be included in the +value returned by the next call to ``read()``, regardless of the +``size`` argument, since they are available without reading any new +bytes from the stream: + + >>> reader.seek(0) # rewind to the start. + >>> reader.readline() # stores extra text in a buffer + 'This is a test file.\n' + >>> print(reader.linebuffer) # examine the buffer contents + ['It is encoded i'] + >>> reader.read(0) # returns the contents of the buffer + 'It is encoded i' + >>> print(reader.linebuffer) # examine the buffer contents + None + +Seek and Tell +------------- +In addition to these basic read operations, +`SeekableUnicodeStreamReader` also supports the ``seek()`` and +``tell()`` operations. However, some care must still be taken when +using these operations. In particular, the only file offsets that +should be passed to ``seek()`` are ``0`` and any offset that has been +returned by ``tell``. + + >>> stream = BytesIO(b"""\ + ... This is a test file. + ... It is encoded in utf-16. + ... """.decode('ascii').encode('utf-16')) + >>> reader = SeekableUnicodeStreamReader(stream, 'utf-16') + >>> reader.read(20) + 'This is a ' + >>> pos = reader.tell(); print(pos) + 22 + >>> reader.read(20) + 'test file.' + >>> reader.seek(pos) # rewind to the position from tell. + >>> reader.read(20) + 'test file.' + +The ``seek()`` and ``tell()`` methods work property even when +``readline()`` is used. + + >>> stream = BytesIO(b"""\ + ... This is a test file. + ... It is encoded in utf-16. + ... """.decode('ascii').encode('utf-16')) + >>> reader = SeekableUnicodeStreamReader(stream, 'utf-16') + >>> reader.readline() + 'This is a test file.\n' + >>> pos = reader.tell(); print(pos) + 44 + >>> reader.readline() + 'It is encoded in utf-16.\n' + >>> reader.seek(pos) # rewind to the position from tell. + >>> reader.readline() + 'It is encoded in utf-16.\n' + + +Squashed Bugs +============= + +svn 5276 fixed a bug in the comment-stripping behavior of +parse_sexpr_block. + + >>> from io import StringIO + >>> from nltk.corpus.reader.util import read_sexpr_block + >>> f = StringIO(b""" + ... (a b c) + ... # This line is a comment. + ... (d e f\ng h)""".decode('ascii')) + >>> print(read_sexpr_block(f, block_size=38, comment_char='#')) + ['(a b c)'] + >>> print(read_sexpr_block(f, block_size=38, comment_char='#')) + ['(d e f\ng h)'] + +svn 5277 fixed a bug in parse_sexpr_block, which would cause it to +enter an infinite loop if a file ended mid-sexpr, or ended with a +token that was not followed by whitespace. A related bug caused +an infinite loop if the corpus ended in an unmatched close paren -- +this was fixed in svn 5279 + + >>> f = StringIO(b""" + ... This file ends mid-sexpr + ... (hello (world""".decode('ascii')) + >>> for i in range(3): print(read_sexpr_block(f)) + ['This', 'file', 'ends', 'mid-sexpr'] + ['(hello (world'] + [] + + >>> f = StringIO(b"This file has no trailing whitespace.".decode('ascii')) + >>> for i in range(3): print(read_sexpr_block(f)) + ['This', 'file', 'has', 'no', 'trailing'] + ['whitespace.'] + [] + + >>> # Bug fixed in 5279: + >>> f = StringIO(b"a b c)".decode('ascii')) + >>> for i in range(3): print(read_sexpr_block(f)) + ['a', 'b'] + ['c)'] + [] + + +svn 5624 & 5265 fixed a bug in ConcatenatedCorpusView, which caused it +to return the wrong items when indexed starting at any index beyond +the first file. + + >>> import nltk + >>> sents = nltk.corpus.brown.sents() + >>> print(sents[6000]) + ['Cholesterol', 'and', 'thyroid'] + >>> print(sents[6000]) + ['Cholesterol', 'and', 'thyroid'] + +svn 5728 fixed a bug in Categorized*CorpusReader, which caused them +to return words from *all* files when just one file was specified. + + >>> from nltk.corpus import reuters + >>> reuters.words('training/13085') + ['SNYDER', '&', 'lt', ';', 'SOI', '>', 'MAKES', ...] + >>> reuters.words('training/5082') + ['SHEPPARD', 'RESOURCES', 'TO', 'MERGE', 'WITH', ...] + +svn 7227 fixed a bug in the qc corpus reader, which prevented +access to its tuples() method + + >>> from nltk.corpus import qc + >>> qc.tuples('test.txt') + [('NUM:dist', 'How far is it from Denver to Aspen ?'), ('LOC:city', 'What county is Modesto , California in ?'), ...] + +Ensure that KEYWORD from `comparative_sents.py` no longer contains a ReDoS vulnerability. + + >>> import re + >>> import time + >>> from nltk.corpus.reader.comparative_sents import KEYWORD + >>> sizes = { + ... "short": 4000, + ... "long": 40000 + ... } + >>> exec_times = { + ... "short": [], + ... "long": [], + ... } + >>> for size_name, size in sizes.items(): + ... for j in range(9): + ... start_t = time.perf_counter() + ... payload = "( " + "(" * size + ... output = KEYWORD.findall(payload) + ... exec_times[size_name].append(time.perf_counter() - start_t) + ... exec_times[size_name] = sorted(exec_times[size_name])[4] # Get the median + +Ideally, the execution time of such a regular expression is linear +in the length of the input. As such, we would expect exec_times["long"] +to be roughly 10 times as big as exec_times["short"]. +With the ReDoS in place, it took roughly 80 times as long. +For now, we accept values below 30 (times as long), due to the potential +for variance. This ensures that the ReDoS has certainly been reduced, +if not removed. + + >>> exec_times["long"] / exec_times["short"] < 30 # doctest: +SKIP + True diff --git a/nltk/test/crubadan.doctest b/nltk/test/crubadan.doctest new file mode 100644 index 0000000..5841778 --- /dev/null +++ b/nltk/test/crubadan.doctest @@ -0,0 +1,65 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +Crubadan Corpus Reader +====================== + +Crubadan is an NLTK corpus reader for ngram files provided +by the Crubadan project. It supports several languages. + + >>> from nltk.corpus import crubadan + >>> crubadan.langs() + ['abk', 'abn',..., 'zpa', 'zul'] + +---------------------------------------- +Language code mapping and helper methods +---------------------------------------- + +The web crawler that generates the 3-gram frequencies works at the +level of "writing systems" rather than languages. Writing systems +are assigned internal 2-3 letter codes that require mapping to the +standard ISO 639-3 codes. For more information, please refer to +the README in nltk_data/crubadan folder after installing it. + +To translate ISO 639-3 codes to "Crubadan Code": + + >>> crubadan.iso_to_crubadan('eng') + 'en' + >>> crubadan.iso_to_crubadan('fra') + 'fr' + >>> crubadan.iso_to_crubadan('aaa') + +In reverse, print ISO 639-3 code if we have the Crubadan Code: + + >>> crubadan.crubadan_to_iso('en') + 'eng' + >>> crubadan.crubadan_to_iso('fr') + 'fra' + >>> crubadan.crubadan_to_iso('aa') + +--------------------------- +Accessing ngram frequencies +--------------------------- + +On initialization the reader will create a dictionary of every +language supported by the Crubadan project, mapping the ISO 639-3 +language code to its corresponding ngram frequency. + +You can access individual language FreqDist and the ngrams within them as follows: + + >>> english_fd = crubadan.lang_freq('eng') + >>> english_fd['the'] + 728135 + +Above accesses the FreqDist of English and returns the frequency of the ngram 'the'. +A ngram that isn't found within the language will return 0: + + >>> english_fd['sometest'] + 0 + +A language that isn't supported will raise an exception: + + >>> crubadan.lang_freq('elvish') + Traceback (most recent call last): + ... + RuntimeError: Unsupported language. diff --git a/nltk/test/data.doctest b/nltk/test/data.doctest new file mode 100644 index 0000000..8f4a564 --- /dev/null +++ b/nltk/test/data.doctest @@ -0,0 +1,390 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +========================================= + Loading Resources From the Data Package +========================================= + + >>> import nltk.data + +Overview +~~~~~~~~ +The `nltk.data` module contains functions that can be used to load +NLTK resource files, such as corpora, grammars, and saved processing +objects. + +Loading Data Files +~~~~~~~~~~~~~~~~~~ +Resources are loaded using the function `nltk.data.load()`, which +takes as its first argument a URL specifying what file should be +loaded. The ``nltk:`` protocol loads files from the NLTK data +distribution. + +However, since July 2024, unpickling is restricted to simple types, +and now fails with a pickle.Unpickling Error. +Instead, all the unsafe pickle packages are now replaced by classes: + + >>> from nltk.tokenize import PunktTokenizer + >>> tokenizer = PunktTokenizer() + + >>> tokenizer.tokenize('Hello. This is a test. It works!') + ['Hello.', 'This is a test.', 'It works!'] + +It is important to note that there should be no space following the +colon (':') in the URL; 'nltk: tokenizers/punkt/english.pickle' will +not work! + +The ``nltk:`` protocol is used by default if no protocol is specified. + +But it is also possible to load resources from ``http:``, ``ftp:``, +and ``file:`` URLs: + + >>> # Load a grammar from the NLTK webpage. + >>> cfg = nltk.data.load('https://raw.githubusercontent.com/nltk/nltk/develop/nltk/test/toy.cfg') + >>> print(cfg) # doctest: +ELLIPSIS + Grammar with 14 productions (start state = S) + S -> NP VP + PP -> P NP + ... + P -> 'on' + P -> 'in' + + >>> # Load a grammar using an absolute path. + >>> url = 'file:%s' % nltk.data.find('grammars/sample_grammars/toy.cfg') + >>> url.replace('\\', '/') + 'file:...toy.cfg' + >>> print(nltk.data.load(url)) + Grammar with 14 productions (start state = S) + S -> NP VP + PP -> P NP + ... + P -> 'on' + P -> 'in' + +The second argument to the `nltk.data.load()` function specifies the +file format, which determines how the file's contents are processed +before they are returned by ``load()``. The formats that are +currently supported by the data module are described by the dictionary +`nltk.data.FORMATS`: + + >>> for format, descr in sorted(nltk.data.FORMATS.items()): + ... print('{0:<7} {1:}'.format(format, descr)) + cfg A context free grammar. + fcfg A feature CFG. + fol A list of first order logic expressions, parsed with + nltk.sem.logic.Expression.fromstring. + json A serialized python object, stored using the json module. + logic A list of first order logic expressions, parsed with + nltk.sem.logic.LogicParser. Requires an additional logic_parser + parameter + pcfg A probabilistic CFG. + pickle A serialized python object, stored using the pickle + module. + raw The raw (byte string) contents of a file. + text The raw (unicode string) contents of a file. + val A semantic valuation, parsed by + nltk.sem.Valuation.fromstring. + yaml A serialized python object, stored using the yaml module. + +`nltk.data.load()` will raise a ValueError if a bad format name is +specified: + + >>> nltk.data.load('grammars/sample_grammars/toy.cfg', 'bar') + Traceback (most recent call last): + . . . + ValueError: Unknown format type! + +By default, the ``"auto"`` format is used, which chooses a format +based on the filename's extension. The mapping from file extensions +to format names is specified by `nltk.data.AUTO_FORMATS`: + + >>> for ext, format in sorted(nltk.data.AUTO_FORMATS.items()): + ... print('.%-7s -> %s' % (ext, format)) + .cfg -> cfg + .fcfg -> fcfg + .fol -> fol + .json -> json + .logic -> logic + .pcfg -> pcfg + .pickle -> pickle + .text -> text + .txt -> text + .val -> val + .yaml -> yaml + +If `nltk.data.load()` is unable to determine the format based on the +filename's extension, it will raise a ValueError: + + >>> nltk.data.load('foo.bar') + Traceback (most recent call last): + . . . + ValueError: Could not determine format for foo.bar based on its file + extension; use the "format" argument to specify the format explicitly. + +Note that by explicitly specifying the ``format`` argument, you can +override the load method's default processing behavior. For example, +to get the raw contents of any file, simply use ``format="raw"``: + + >>> s = nltk.data.load('grammars/sample_grammars/toy.cfg', 'text') + >>> print(s) + S -> NP VP + PP -> P NP + NP -> Det N | NP PP + VP -> V NP | VP PP + ... + +Making Local Copies +~~~~~~~~~~~~~~~~~~~ +.. This will not be visible in the html output: create a tempdir to + play in. + >>> import tempfile, os + >>> tempdir = tempfile.mkdtemp() + >>> old_dir = os.path.abspath('.') + >>> os.chdir(tempdir) + +The function `nltk.data.retrieve()` copies a given resource to a local +file. This can be useful, for example, if you want to edit one of the +sample grammars. + + >>> nltk.data.retrieve('grammars/sample_grammars/toy.cfg') + Retrieving 'nltk:grammars/sample_grammars/toy.cfg', saving to 'toy.cfg' + + >>> # Simulate editing the grammar. + >>> with open('toy.cfg') as inp: + ... s = inp.read().replace('NP', 'DP') + >>> with open('toy.cfg', 'w') as out: + ... _bytes_written = out.write(s) + + >>> # Load the edited grammar, & display it. + >>> cfg = nltk.data.load('file:///' + os.path.abspath('toy.cfg')) + >>> print(cfg) + Grammar with 14 productions (start state = S) + S -> DP VP + PP -> P DP + ... + P -> 'on' + P -> 'in' + +The second argument to `nltk.data.retrieve()` specifies the filename +for the new copy of the file. By default, the source file's filename +is used. + + >>> nltk.data.retrieve('grammars/sample_grammars/toy.cfg', 'mytoy.cfg') + Retrieving 'nltk:grammars/sample_grammars/toy.cfg', saving to 'mytoy.cfg' + >>> os.path.isfile('./mytoy.cfg') + True + >>> nltk.data.retrieve('grammars/sample_grammars/np.fcfg') + Retrieving 'nltk:grammars/sample_grammars/np.fcfg', saving to 'np.fcfg' + >>> os.path.isfile('./np.fcfg') + True + +If a file with the specified (or default) filename already exists in +the current directory, then `nltk.data.retrieve()` will raise a +ValueError exception. It will *not* overwrite the file: + + >>> os.path.isfile('./toy.cfg') + True + >>> nltk.data.retrieve('grammars/sample_grammars/toy.cfg') + Traceback (most recent call last): + . . . + ValueError: File '...toy.cfg' already exists! + +.. This will not be visible in the html output: clean up the tempdir. + >>> os.chdir(old_dir) + >>> for f in os.listdir(tempdir): + ... os.remove(os.path.join(tempdir, f)) + >>> os.rmdir(tempdir) + +Finding Files in the NLTK Data Package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The `nltk.data.find()` function searches the NLTK data package for a +given file, and returns a pointer to that file. This pointer can +either be a `FileSystemPathPointer` (whose `path` attribute gives the +absolute path of the file); or a `ZipFilePathPointer`, specifying a +zipfile and the name of an entry within that zipfile. Both pointer +types define the `open()` method, which can be used to read the string +contents of the file. + + >>> path = nltk.data.find('corpora/abc/rural.txt') + >>> str(path) + '...rural.txt' + >>> print(path.open().read(60).decode()) + PM denies knowledge of AWB kickbacks + The Prime Minister has + +Alternatively, the `nltk.data.load()` function can be used with the +keyword argument ``format="raw"``: + + >>> s = nltk.data.load('corpora/abc/rural.txt', format='raw')[:60] + >>> print(s.decode()) + PM denies knowledge of AWB kickbacks + The Prime Minister has + +Alternatively, you can use the keyword argument ``format="text"``: + + >>> s = nltk.data.load('corpora/abc/rural.txt', format='text')[:60] + >>> print(s) + PM denies knowledge of AWB kickbacks + The Prime Minister has + +Resource Caching +~~~~~~~~~~~~~~~~ + +NLTK uses a weakref dictionary to maintain a cache of resources that +have been loaded. If you load a resource that is already stored in +the cache, then the cached copy will be returned. This behavior can +be seen by the trace output generated when verbose=True: + + >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg', verbose=True) + <> + >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg', verbose=True) + <> + +If you wish to load a resource from its source, bypassing the cache, +use the ``cache=False`` argument to `nltk.data.load()`. This can be +useful, for example, if the resource is loaded from a local file, and +you are actively editing that file: + + >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg',cache=False,verbose=True) + <> + +The cache *no longer* uses weak references. A resource will not be +automatically expunged from the cache when no more objects are using +it. In the following example, when we clear the variable ``feat0``, +the reference count for the feature grammar object drops to zero. +However, the object remains cached: + + >>> del feat0 + >>> feat0 = nltk.data.load('grammars/book_grammars/feat0.fcfg', + ... verbose=True) + <> + +You can clear the entire contents of the cache, using +`nltk.data.clear_cache()`: + + >>> nltk.data.clear_cache() + +Retrieving other Data Sources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + >>> formulas = nltk.data.load('grammars/book_grammars/background.fol') + >>> for f in formulas: print(str(f)) + all x.(boxerdog(x) -> dog(x)) + all x.(boxer(x) -> person(x)) + all x.-(dog(x) & person(x)) + all x.(married(x) <-> exists y.marry(x,y)) + all x.(bark(x) -> dog(x)) + all x y.(marry(x,y) -> (person(x) & person(y))) + -(Vincent = Mia) + -(Vincent = Fido) + -(Mia = Fido) + +Regression Tests +~~~~~~~~~~~~~~~~ +Create a temp dir for tests that write files: + + >>> import tempfile, os + >>> tempdir = tempfile.mkdtemp() + >>> old_dir = os.path.abspath('.') + >>> os.chdir(tempdir) + +The `retrieve()` function accepts all url types: + + >>> urls = ['https://raw.githubusercontent.com/nltk/nltk/develop/nltk/test/toy.cfg', + ... 'file:%s' % nltk.data.find('grammars/sample_grammars/toy.cfg'), + ... 'nltk:grammars/sample_grammars/toy.cfg', + ... 'grammars/sample_grammars/toy.cfg'] + >>> for i, url in enumerate(urls): + ... nltk.data.retrieve(url, 'toy-%d.cfg' % i) + Retrieving 'https://raw.githubusercontent.com/nltk/nltk/develop/nltk/test/toy.cfg', saving to 'toy-0.cfg' + Retrieving 'file:...toy.cfg', saving to 'toy-1.cfg' + Retrieving 'nltk:grammars/sample_grammars/toy.cfg', saving to 'toy-2.cfg' + Retrieving 'nltk:grammars/sample_grammars/toy.cfg', saving to 'toy-3.cfg' + +Clean up the temp dir: + + >>> os.chdir(old_dir) + >>> for f in os.listdir(tempdir): + ... os.remove(os.path.join(tempdir, f)) + >>> os.rmdir(tempdir) + +Lazy Loader +----------- +A lazy loader is a wrapper object that defers loading a resource until +it is accessed or used in any way. This is mainly intended for +internal use by NLTK's corpus readers. + + >>> # Create a lazy loader for toy.cfg. + >>> ll = nltk.data.LazyLoader('grammars/sample_grammars/toy.cfg') + + >>> # Show that it's not loaded yet: + >>> object.__repr__(ll) + '' + + >>> # printing it is enough to cause it to be loaded: + >>> print(ll) + + + >>> # Show that it's now been loaded: + >>> object.__repr__(ll) + '' + + + >>> # Test that accessing an attribute also loads it: + >>> ll = nltk.data.LazyLoader('grammars/sample_grammars/toy.cfg') + >>> ll.start() + S + >>> object.__repr__(ll) + '' + +Buffered Gzip Reading and Writing +--------------------------------- +Write performance to gzip-compressed is extremely poor when the files become large. +File creation can become a bottleneck in those cases. + +Read performance from large gzipped pickle files was improved in data.py by +buffering the reads. A similar fix can be applied to writes by buffering +the writes to a StringIO object first. + +This is mainly intended for internal use. The test simply tests that reading +and writing work as intended and does not test how much improvement buffering +provides. + + >>> from io import StringIO + >>> test = nltk.data.BufferedGzipFile('testbuf.gz', 'wb', size=2**10) + >>> ans = [] + >>> for i in range(10000): + ... ans.append(str(i).encode('ascii')) + ... test.write(str(i).encode('ascii')) + >>> test.close() + >>> test = nltk.data.BufferedGzipFile('testbuf.gz', 'rb') + >>> test.read() == b''.join(ans) + True + >>> test.close() + >>> import os + >>> os.unlink('testbuf.gz') + +JSON Encoding and Decoding +-------------------------- +JSON serialization is used instead of pickle for some classes. + + >>> from nltk import jsontags + >>> from nltk.jsontags import JSONTaggedEncoder, JSONTaggedDecoder, register_tag + >>> @jsontags.register_tag + ... class JSONSerializable: + ... json_tag = 'JSONSerializable' + ... + ... def __init__(self, n): + ... self.n = n + ... + ... def encode_json_obj(self): + ... return self.n + ... + ... @classmethod + ... def decode_json_obj(cls, obj): + ... n = obj + ... return cls(n) + ... + >>> JSONTaggedEncoder().encode(JSONSerializable(1)) + '{"!JSONSerializable": 1}' + >>> JSONTaggedDecoder().decode('{"!JSONSerializable": 1}').n + 1 diff --git a/nltk/test/dependency.doctest b/nltk/test/dependency.doctest new file mode 100755 index 0000000..9e06210 --- /dev/null +++ b/nltk/test/dependency.doctest @@ -0,0 +1,241 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +=================== +Dependency Grammars +=================== + + >>> from nltk.grammar import DependencyGrammar + >>> from nltk.parse import ( + ... DependencyGraph, + ... ProjectiveDependencyParser, + ... NonprojectiveDependencyParser, + ... ) + +CoNLL Data +---------- + + >>> treebank_data = """Pierre NNP 2 NMOD + ... Vinken NNP 8 SUB + ... , , 2 P + ... 61 CD 5 NMOD + ... years NNS 6 AMOD + ... old JJ 2 NMOD + ... , , 2 P + ... will MD 0 ROOT + ... join VB 8 VC + ... the DT 11 NMOD + ... board NN 9 OBJ + ... as IN 9 VMOD + ... a DT 15 NMOD + ... nonexecutive JJ 15 NMOD + ... director NN 12 PMOD + ... Nov. NNP 9 VMOD + ... 29 CD 16 NMOD + ... . . 9 VMOD + ... """ + + >>> dg = DependencyGraph(treebank_data) + >>> dg.tree().pprint() + (will + (Vinken Pierre , (old (years 61)) ,) + (join (board the) (as (director a nonexecutive)) (Nov. 29) .)) + >>> for head, rel, dep in dg.triples(): + ... print( + ... '({h[0]}, {h[1]}), {r}, ({d[0]}, {d[1]})' + ... .format(h=head, r=rel, d=dep) + ... ) + (will, MD), SUB, (Vinken, NNP) + (Vinken, NNP), NMOD, (Pierre, NNP) + (Vinken, NNP), P, (,, ,) + (Vinken, NNP), NMOD, (old, JJ) + (old, JJ), AMOD, (years, NNS) + (years, NNS), NMOD, (61, CD) + (Vinken, NNP), P, (,, ,) + (will, MD), VC, (join, VB) + (join, VB), OBJ, (board, NN) + (board, NN), NMOD, (the, DT) + (join, VB), VMOD, (as, IN) + (as, IN), PMOD, (director, NN) + (director, NN), NMOD, (a, DT) + (director, NN), NMOD, (nonexecutive, JJ) + (join, VB), VMOD, (Nov., NNP) + (Nov., NNP), NMOD, (29, CD) + (join, VB), VMOD, (., .) + +Using a custom cell extractor. + + >>> def custom_extractor(cells): + ... _, tag, head, rel = cells + ... return 'spam', 'spam', tag, tag, '', head, rel + >>> dg = DependencyGraph(treebank_data, cell_extractor=custom_extractor) + >>> dg.tree().pprint() + (spam + (spam spam spam (spam (spam spam)) spam) + (spam (spam spam) (spam (spam spam spam)) (spam spam) spam)) + +Custom cell extractors can take in and return an index. + + >>> def custom_extractor(cells, index): + ... word, tag, head, rel = cells + ... return (index, '{}-{}'.format(word, index), word, + ... tag, tag, '', head, rel) + >>> dg = DependencyGraph(treebank_data, cell_extractor=custom_extractor) + >>> dg.tree().pprint() + (will-8 + (Vinken-2 Pierre-1 ,-3 (old-6 (years-5 61-4)) ,-7) + (join-9 + (board-11 the-10) + (as-12 (director-15 a-13 nonexecutive-14)) + (Nov.-16 29-17) + .-18)) + +Using the dependency-parsed version of the Penn Treebank corpus sample. + + >>> from nltk.corpus import dependency_treebank + >>> t = dependency_treebank.parsed_sents()[0] + >>> print(t.to_conll(3)) + Pierre NNP 2 + Vinken NNP 8 + , , 2 + 61 CD 5 + years NNS 6 + old JJ 2 + , , 2 + will MD 0 + join VB 8 + the DT 11 + board NN 9 + as IN 9 + a DT 15 + nonexecutive JJ 15 + director NN 12 + Nov. NNP 9 + 29 CD 16 + . . 8 + +Using the output of zpar (like Malt-TAB but with zero-based indexing) + + >>> zpar_data = """ + ... Pierre NNP 1 NMOD + ... Vinken NNP 7 SUB + ... , , 1 P + ... 61 CD 4 NMOD + ... years NNS 5 AMOD + ... old JJ 1 NMOD + ... , , 1 P + ... will MD -1 ROOT + ... join VB 7 VC + ... the DT 10 NMOD + ... board NN 8 OBJ + ... as IN 8 VMOD + ... a DT 14 NMOD + ... nonexecutive JJ 14 NMOD + ... director NN 11 PMOD + ... Nov. NNP 8 VMOD + ... 29 CD 15 NMOD + ... . . 7 P + ... """ + + >>> zdg = DependencyGraph(zpar_data, zero_based=True) + >>> print(zdg.tree()) + (will + (Vinken Pierre , (old (years 61)) ,) + (join (board the) (as (director a nonexecutive)) (Nov. 29)) + .) + + +Projective Dependency Parsing +----------------------------- + + >>> grammar = DependencyGrammar.fromstring(""" + ... 'fell' -> 'price' | 'stock' + ... 'price' -> 'of' 'the' + ... 'of' -> 'stock' + ... 'stock' -> 'the' + ... """) + >>> print(grammar) + Dependency grammar with 5 productions + 'fell' -> 'price' + 'fell' -> 'stock' + 'price' -> 'of' 'the' + 'of' -> 'stock' + 'stock' -> 'the' + + >>> dp = ProjectiveDependencyParser(grammar) + >>> for t in sorted(dp.parse(['the', 'price', 'of', 'the', 'stock', 'fell'])): + ... print(t) + (fell (price the (of (stock the)))) + (fell (price the of) (stock the)) + (fell (price the of the) stock) + +Non-Projective Dependency Parsing +--------------------------------- + + >>> grammar = DependencyGrammar.fromstring(""" + ... 'taught' -> 'play' | 'man' + ... 'man' -> 'the' + ... 'play' -> 'golf' | 'dog' | 'to' + ... 'dog' -> 'his' + ... """) + >>> print(grammar) + Dependency grammar with 7 productions + 'taught' -> 'play' + 'taught' -> 'man' + 'man' -> 'the' + 'play' -> 'golf' + 'play' -> 'dog' + 'play' -> 'to' + 'dog' -> 'his' + + >>> dp = NonprojectiveDependencyParser(grammar) + >>> g, = dp.parse(['the', 'man', 'taught', 'his', 'dog', 'to', 'play', 'golf']) + + >>> print(g.root['word']) + taught + + >>> for _, node in sorted(g.nodes.items()): + ... if node['word'] is not None: + ... print('{address} {word}: {d}'.format(d=node['deps'][''], **node)) + 1 the: [] + 2 man: [1] + 3 taught: [2, 7] + 4 his: [] + 5 dog: [4] + 6 to: [] + 7 play: [5, 6, 8] + 8 golf: [] + + >>> print(g.tree()) + (taught (man the) (play (dog his) to golf)) + +Integration with MALT parser +============================ + +In case the top relation is different from the default, we can set it. In case +of MALT parser, it's set to `'null'`. + +>>> dg_str = """1 I _ NN NN _ 2 nn _ _ +... 2 shot _ NN NN _ 0 null _ _ +... 3 an _ AT AT _ 2 dep _ _ +... 4 elephant _ NN NN _ 7 nn _ _ +... 5 in _ NN NN _ 7 nn _ _ +... 6 my _ NN NN _ 7 nn _ _ +... 7 pajamas _ NNS NNS _ 3 dobj _ _ +... """ +>>> dg = DependencyGraph(dg_str, top_relation_label='null') + +>>> len(dg.nodes) +8 + +>>> dg.root['word'], dg.root['address'] +('shot', 2) + +>>> print(dg.to_conll(10)) +1 I _ NN NN _ 2 nn _ _ +2 shot _ NN NN _ 0 null _ _ +3 an _ AT AT _ 2 dep _ _ +4 elephant _ NN NN _ 7 nn _ _ +5 in _ NN NN _ 7 nn _ _ +6 my _ NN NN _ 7 nn _ _ +7 pajamas _ NNS NNS _ 3 dobj _ _ diff --git a/nltk/test/discourse.doctest b/nltk/test/discourse.doctest new file mode 100644 index 0000000..91a38c4 --- /dev/null +++ b/nltk/test/discourse.doctest @@ -0,0 +1,552 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +================== +Discourse Checking +================== + + >>> from nltk import * + >>> from nltk.sem import logic + >>> logic._counter._value = 0 + +Setup +===== + + >>> from nltk.test.childes_fixt import setup_module + >>> setup_module() + +Introduction +============ + +The NLTK discourse module makes it possible to test consistency and +redundancy of simple discourses, using theorem-proving and +model-building from `nltk.inference`. + +The ``DiscourseTester`` constructor takes a list of sentences as a +parameter. + + >>> dt = DiscourseTester(['a boxer walks', 'every boxer chases a girl']) + +The ``DiscourseTester`` parses each sentence into a list of logical +forms. Once we have created ``DiscourseTester`` object, we can +inspect various properties of the discourse. First off, we might want +to double-check what sentences are currently stored as the discourse. + + >>> dt.sentences() + s0: a boxer walks + s1: every boxer chases a girl + +As you will see, each sentence receives an identifier `s`\ :subscript:`i`. +We might also want to check what grammar the ``DiscourseTester`` is +using (by default, ``book_grammars/discourse.fcfg``): + + >>> dt.grammar() + % start S + # Grammar Rules + S[SEM = ] -> NP[NUM=?n,SEM=?subj] VP[NUM=?n,SEM=?vp] + NP[NUM=?n,SEM= ] -> Det[NUM=?n,SEM=?det] Nom[NUM=?n,SEM=?nom] + NP[LOC=?l,NUM=?n,SEM=?np] -> PropN[LOC=?l,NUM=?n,SEM=?np] + ... + +A different grammar can be invoked by using the optional ``gramfile`` +parameter when a ``DiscourseTester`` object is created. + +Readings and Threads +==================== + +Depending on +the grammar used, we may find some sentences have more than one +logical form. To check this, use the ``readings()`` method. Given a +sentence identifier of the form `s`\ :subscript:`i`, each reading of +that sentence is given an identifier `s`\ :sub:`i`-`r`\ :sub:`j`. + + + >>> dt.readings() + + s0 readings: + + s0-r0: exists z1.(boxer(z1) & walk(z1)) + s0-r1: exists z1.(boxerdog(z1) & walk(z1)) + + s1 readings: + + s1-r0: all z2.(boxer(z2) -> exists z3.(girl(z3) & chase(z2,z3))) + s1-r1: all z1.(boxerdog(z1) -> exists z2.(girl(z2) & chase(z1,z2))) + + +In this case, the only source of ambiguity lies in the word *boxer*, +which receives two translations: ``boxer`` and ``boxerdog``. The +intention is that one of these corresponds to the ``person`` sense and +one to the ``dog`` sense. In principle, we would also expect to see a +quantifier scope ambiguity in ``s1``. However, the simple grammar we +are using, namely `sem4.fcfg `_, doesn't support quantifier +scope ambiguity. + +We can also investigate the readings of a specific sentence: + + >>> dt.readings('a boxer walks') + The sentence 'a boxer walks' has these readings: + exists x.(boxer(x) & walk(x)) + exists x.(boxerdog(x) & walk(x)) + +Given that each sentence is two-ways ambiguous, we potentially have +four different discourse 'threads', taking all combinations of +readings. To see these, specify the ``threaded=True`` parameter on +the ``readings()`` method. Again, each thread is assigned an +identifier of the form `d`\ :sub:`i`. Following the identifier is a +list of the readings that constitute that thread. + + >>> dt.readings(threaded=True) + d0: ['s0-r0', 's1-r0'] + d1: ['s0-r0', 's1-r1'] + d2: ['s0-r1', 's1-r0'] + d3: ['s0-r1', 's1-r1'] + +Of course, this simple-minded approach doesn't scale: a discourse with, say, three +sentences, each of which has 3 readings, will generate 27 different +threads. It is an interesting exercise to consider how to manage +discourse ambiguity more efficiently. + +Checking Consistency +==================== + +Now, we can check whether some or all of the discourse threads are +consistent, using the ``models()`` method. With no parameter, this +method will try to find a model for every discourse thread in the +current discourse. However, we can also specify just one thread, say ``d1``. + + >>> dt.models('d1') + -------------------------------------------------------------------------------- + Model for Discourse Thread d1 + -------------------------------------------------------------------------------- + % number = 1 + % seconds = 0 + + % Interpretation of size 2 + + c1 = 0. + + f1(0) = 0. + f1(1) = 0. + + boxer(0). + - boxer(1). + + - boxerdog(0). + - boxerdog(1). + + - girl(0). + - girl(1). + + walk(0). + - walk(1). + + - chase(0,0). + - chase(0,1). + - chase(1,0). + - chase(1,1). + + Consistent discourse: d1 ['s0-r0', 's1-r1']: + s0-r0: exists z1.(boxer(z1) & walk(z1)) + s1-r1: all z1.(boxerdog(z1) -> exists z2.(girl(z2) & chase(z1,z2))) + + +There are various formats for rendering **Mace4** models --- here, +we have used the 'cooked' format (which is intended to be +human-readable). There are a number of points to note. + +#. The entities in the domain are all treated as non-negative + integers. In this case, there are only two entities, ``0`` and + ``1``. + +#. The ``-`` symbol indicates negation. So ``0`` is the only + ``boxerdog`` and the only thing that ``walk``\ s. Nothing is a + ``boxer``, or a ``girl`` or in the ``chase`` relation. Thus the + universal sentence is vacuously true. + +#. ``c1`` is an introduced constant that denotes ``0``. + +#. ``f1`` is a Skolem function, but it plays no significant role in + this model. + + +We might want to now add another sentence to the discourse, and there +is method ``add_sentence()`` for doing just this. + + >>> dt.add_sentence('John is a boxer') + >>> dt.sentences() + s0: a boxer walks + s1: every boxer chases a girl + s2: John is a boxer + +We can now test all the properties as before; here, we just show a +couple of them. + + >>> dt.readings() + + s0 readings: + + s0-r0: exists z1.(boxer(z1) & walk(z1)) + s0-r1: exists z1.(boxerdog(z1) & walk(z1)) + + s1 readings: + + s1-r0: all z1.(boxer(z1) -> exists z2.(girl(z2) & chase(z1,z2))) + s1-r1: all z1.(boxerdog(z1) -> exists z2.(girl(z2) & chase(z1,z2))) + + s2 readings: + + s2-r0: boxer(John) + s2-r1: boxerdog(John) + >>> dt.readings(threaded=True) + d0: ['s0-r0', 's1-r0', 's2-r0'] + d1: ['s0-r0', 's1-r0', 's2-r1'] + d2: ['s0-r0', 's1-r1', 's2-r0'] + d3: ['s0-r0', 's1-r1', 's2-r1'] + d4: ['s0-r1', 's1-r0', 's2-r0'] + d5: ['s0-r1', 's1-r0', 's2-r1'] + d6: ['s0-r1', 's1-r1', 's2-r0'] + d7: ['s0-r1', 's1-r1', 's2-r1'] + +If you are interested in a particular thread, the ``expand_threads()`` +method will remind you of what readings it consists of: + + >>> thread = dt.expand_threads('d1') + >>> for rid, reading in thread: + ... print(rid, str(reading.normalize())) + s0-r0 exists z1.(boxer(z1) & walk(z1)) + s1-r0 all z1.(boxer(z1) -> exists z2.(girl(z2) & chase(z1,z2))) + s2-r1 boxerdog(John) + +Suppose we have already defined a discourse, as follows: + + >>> dt = DiscourseTester(['A student dances', 'Every student is a person']) + +Now, when we add a new sentence, is it consistent with what we already +have? The `` consistchk=True`` parameter of ``add_sentence()`` allows +us to check: + + >>> dt.add_sentence('No person dances', consistchk=True) + Inconsistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0']: + s0-r0: exists z1.(student(z1) & dance(z1)) + s1-r0: all z1.(student(z1) -> person(z1)) + s2-r0: -exists z1.(person(z1) & dance(z1)) + + >>> dt.readings() + + s0 readings: + + s0-r0: exists z1.(student(z1) & dance(z1)) + + s1 readings: + + s1-r0: all z1.(student(z1) -> person(z1)) + + s2 readings: + + s2-r0: -exists z1.(person(z1) & dance(z1)) + +So let's retract the inconsistent sentence: + + >>> dt.retract_sentence('No person dances', verbose=True) + Current sentences are + s0: A student dances + s1: Every student is a person + +We can now verify that result is consistent. + + >>> dt.models() + -------------------------------------------------------------------------------- + Model for Discourse Thread d0 + -------------------------------------------------------------------------------- + % number = 1 + % seconds = 0 + + % Interpretation of size 2 + + c1 = 0. + + dance(0). + - dance(1). + + person(0). + - person(1). + + student(0). + - student(1). + + Consistent discourse: d0 ['s0-r0', 's1-r0']: + s0-r0: exists z1.(student(z1) & dance(z1)) + s1-r0: all z1.(student(z1) -> person(z1)) + + +Checking Informativity +====================== + +Let's assume that we are still trying to extend the discourse *A +student dances.* *Every student is a person.* We add a new sentence, +but this time, we check whether it is informative with respect to what +has gone before. + + >>> dt.add_sentence('A person dances', informchk=True) + Sentence 'A person dances' under reading 'exists x.(person(x) & dance(x))': + Not informative relative to thread 'd0' + +In fact, we are just checking whether the new sentence is entailed by +the preceding discourse. + + >>> dt.models() + -------------------------------------------------------------------------------- + Model for Discourse Thread d0 + -------------------------------------------------------------------------------- + % number = 1 + % seconds = 0 + + % Interpretation of size 2 + + c1 = 0. + + c2 = 0. + + dance(0). + - dance(1). + + person(0). + - person(1). + + student(0). + - student(1). + + Consistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0']: + s0-r0: exists z1.(student(z1) & dance(z1)) + s1-r0: all z1.(student(z1) -> person(z1)) + s2-r0: exists z1.(person(z1) & dance(z1)) + + + + +Adding Background Knowledge +=========================== + +Let's build a new discourse, and look at the readings of the component sentences: + + >>> dt = DiscourseTester(['Vincent is a boxer', 'Fido is a boxer', 'Vincent is married', 'Fido barks']) + >>> dt.readings() + + s0 readings: + + s0-r0: boxer(Vincent) + s0-r1: boxerdog(Vincent) + + s1 readings: + + s1-r0: boxer(Fido) + s1-r1: boxerdog(Fido) + + s2 readings: + + s2-r0: married(Vincent) + + s3 readings: + + s3-r0: bark(Fido) + +This gives us a lot of threads: + + >>> dt.readings(threaded=True) + d0: ['s0-r0', 's1-r0', 's2-r0', 's3-r0'] + d1: ['s0-r0', 's1-r1', 's2-r0', 's3-r0'] + d2: ['s0-r1', 's1-r0', 's2-r0', 's3-r0'] + d3: ['s0-r1', 's1-r1', 's2-r0', 's3-r0'] + + +We can eliminate some of the readings, and hence some of the threads, +by adding background information. + + >>> import nltk.data + >>> bg = nltk.data.load('grammars/book_grammars/background.fol') + >>> dt.add_background(bg) + >>> dt.background() + all x.(boxerdog(x) -> dog(x)) + all x.(boxer(x) -> person(x)) + all x.-(dog(x) & person(x)) + all x.(married(x) <-> exists y.marry(x,y)) + all x.(bark(x) -> dog(x)) + all x y.(marry(x,y) -> (person(x) & person(y))) + -(Vincent = Mia) + -(Vincent = Fido) + -(Mia = Fido) + +The background information allows us to reject three of the threads as +inconsistent. To see what remains, use the ``filter=True`` parameter +on ``readings()``. + + >>> dt.readings(filter=True) + d1: ['s0-r0', 's1-r1', 's2-r0', 's3-r0'] + +The ``models()`` method gives us more information about the surviving thread. + + >>> dt.models() + -------------------------------------------------------------------------------- + Model for Discourse Thread d0 + -------------------------------------------------------------------------------- + No model found! + + -------------------------------------------------------------------------------- + Model for Discourse Thread d1 + -------------------------------------------------------------------------------- + % number = 1 + % seconds = 0 + + % Interpretation of size 3 + + Fido = 0. + + Mia = 1. + + Vincent = 2. + + f1(0) = 0. + f1(1) = 0. + f1(2) = 2. + + bark(0). + - bark(1). + - bark(2). + + - boxer(0). + - boxer(1). + boxer(2). + + boxerdog(0). + - boxerdog(1). + - boxerdog(2). + + dog(0). + - dog(1). + - dog(2). + + - married(0). + - married(1). + married(2). + + - person(0). + - person(1). + person(2). + + - marry(0,0). + - marry(0,1). + - marry(0,2). + - marry(1,0). + - marry(1,1). + - marry(1,2). + - marry(2,0). + - marry(2,1). + marry(2,2). + + -------------------------------------------------------------------------------- + Model for Discourse Thread d2 + -------------------------------------------------------------------------------- + No model found! + + -------------------------------------------------------------------------------- + Model for Discourse Thread d3 + -------------------------------------------------------------------------------- + No model found! + + Inconsistent discourse: d0 ['s0-r0', 's1-r0', 's2-r0', 's3-r0']: + s0-r0: boxer(Vincent) + s1-r0: boxer(Fido) + s2-r0: married(Vincent) + s3-r0: bark(Fido) + + Consistent discourse: d1 ['s0-r0', 's1-r1', 's2-r0', 's3-r0']: + s0-r0: boxer(Vincent) + s1-r1: boxerdog(Fido) + s2-r0: married(Vincent) + s3-r0: bark(Fido) + + Inconsistent discourse: d2 ['s0-r1', 's1-r0', 's2-r0', 's3-r0']: + s0-r1: boxerdog(Vincent) + s1-r0: boxer(Fido) + s2-r0: married(Vincent) + s3-r0: bark(Fido) + + Inconsistent discourse: d3 ['s0-r1', 's1-r1', 's2-r0', 's3-r0']: + s0-r1: boxerdog(Vincent) + s1-r1: boxerdog(Fido) + s2-r0: married(Vincent) + s3-r0: bark(Fido) + + + +.. This will not be visible in the html output: create a tempdir to + play in. + >>> import tempfile, os + >>> tempdir = tempfile.mkdtemp() + >>> old_dir = os.path.abspath('.') + >>> os.chdir(tempdir) + +In order to play around with your own version of background knowledge, +you might want to start off with a local copy of ``background.fol``: + + >>> nltk.data.retrieve('grammars/book_grammars/background.fol') + Retrieving 'nltk:grammars/book_grammars/background.fol', saving to 'background.fol' + +After you have modified the file, the ``load_fol()`` function will parse +the strings in the file into expressions of ``nltk.sem.logic``. + + >>> from nltk.inference.discourse import load_fol + >>> mybg = load_fol(open('background.fol').read()) + +The result can be loaded as an argument of ``add_background()`` in the +manner shown earlier. + +.. This will not be visible in the html output: clean up the tempdir. + >>> os.chdir(old_dir) + >>> for f in os.listdir(tempdir): + ... os.remove(os.path.join(tempdir, f)) + >>> os.rmdir(tempdir) + >>> nltk.data.clear_cache() + + +Regression Testing from book +============================ + + >>> logic._counter._value = 0 + + >>> from nltk.tag import RegexpTagger + >>> tagger = RegexpTagger( + ... [('^(chases|runs)$', 'VB'), + ... ('^(a)$', 'ex_quant'), + ... ('^(every)$', 'univ_quant'), + ... ('^(dog|boy)$', 'NN'), + ... ('^(He)$', 'PRP') + ... ]) + >>> rc = DrtGlueReadingCommand(depparser=MaltParser(tagger=tagger)) + >>> dt = DiscourseTester(map(str.split, ['Every dog chases a boy', 'He runs']), rc) + >>> dt.readings() + + s0 readings: + + s0-r0: ([z2],[boy(z2), (([z5],[dog(z5)]) -> ([],[chases(z5,z2)]))]) + s0-r1: ([],[(([z1],[dog(z1)]) -> ([z2],[boy(z2), chases(z1,z2)]))]) + + s1 readings: + + s1-r0: ([z1],[PRO(z1), runs(z1)]) + >>> dt.readings(show_thread_readings=True) + d0: ['s0-r0', 's1-r0'] : ([z1,z2],[boy(z1), (([z3],[dog(z3)]) -> ([],[chases(z3,z1)])), (z2 = z1), runs(z2)]) + d1: ['s0-r1', 's1-r0'] : INVALID: AnaphoraResolutionException + >>> dt.readings(filter=True, show_thread_readings=True) + d0: ['s0-r0', 's1-r0'] : ([z1,z3],[boy(z1), (([z2],[dog(z2)]) -> ([],[chases(z2,z1)])), (z3 = z1), runs(z3)]) + + >>> logic._counter._value = 0 + + >>> from nltk.parse import FeatureEarleyChartParser + >>> from nltk.sem.drt import DrtParser + >>> grammar = nltk.data.load('grammars/book_grammars/drt.fcfg', logic_parser=DrtParser()) + >>> parser = FeatureEarleyChartParser(grammar, trace=0) + >>> trees = parser.parse('Angus owns a dog'.split()) + >>> print(list(trees)[0].label()['SEM'].simplify().normalize()) + ([z1,z2],[Angus(z1), dog(z2), own(z1,z2)]) diff --git a/nltk/test/drt.doctest b/nltk/test/drt.doctest new file mode 100644 index 0000000..6d3581d --- /dev/null +++ b/nltk/test/drt.doctest @@ -0,0 +1,515 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +================================ + Discourse Representation Theory +================================ + + >>> from nltk.sem import logic + >>> from nltk.inference import TableauProver + +Overview +======== + +A DRS can be created with the ``DRS()`` constructor. This takes two arguments: a list of +discourse referents and list of conditions. . + + >>> from nltk.sem.drt import * + >>> dexpr = DrtExpression.fromstring + >>> man_x = dexpr('man(x)') + >>> walk_x = dexpr('walk(x)') + >>> x = dexpr('x') + >>> print(DRS([x], [man_x, walk_x])) + ([x],[man(x), walk(x)]) + +The ``parse()`` method can also be applied directly to DRS +expressions, which allows them to be specified more +easily. + + >>> drs1 = dexpr('([x],[man(x),walk(x)])') + >>> print(drs1) + ([x],[man(x), walk(x)]) + +DRSs can be *merged* using the ``+`` operator. + + >>> drs2 = dexpr('([y],[woman(y),stop(y)])') + >>> drs3 = drs1 + drs2 + >>> print(drs3) + (([x],[man(x), walk(x)]) + ([y],[woman(y), stop(y)])) + >>> print(drs3.simplify()) + ([x,y],[man(x), walk(x), woman(y), stop(y)]) + +We can embed DRSs as components of an ``implies`` condition. + + >>> s = '([], [(%s -> %s)])' % (drs1, drs2) + >>> print(dexpr(s)) + ([],[(([x],[man(x), walk(x)]) -> ([y],[woman(y), stop(y)]))]) + +The ``fol()`` method converts DRSs into FOL formulae. + + >>> print(dexpr(r'([x],[man(x), walks(x)])').fol()) + exists x.(man(x) & walks(x)) + >>> print(dexpr(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])').fol()) + all x.(man(x) -> walks(x)) + +In order to visualize a DRS, the ``pretty_format()`` method can be used. + + >>> print(drs3.pretty_format()) + _________ __________ + | x | | y | + (|---------| + |----------|) + | man(x) | | woman(y) | + | walk(x) | | stop(y) | + |_________| |__________| + + +Parse to semantics +------------------ + +.. + >>> logic._counter._value = 0 + +DRSs can be used for building compositional semantics in a feature +based grammar. To specify that we want to use DRSs, the appropriate +logic parser needs be passed as a parameter to ``load_earley()`` + + >>> from nltk.parse import load_parser + >>> from nltk.sem.drt import DrtParser + >>> parser = load_parser('grammars/book_grammars/drt.fcfg', trace=0, logic_parser=DrtParser()) + >>> for tree in parser.parse('a dog barks'.split()): + ... print(tree.label()['SEM'].simplify()) + ... + ([x],[dog(x), bark(x)]) + +Alternatively, a ``FeatStructReader`` can be passed with the ``logic_parser`` set on it + + >>> from nltk.featstruct import FeatStructReader + >>> from nltk.grammar import FeatStructNonterminal + >>> parser = load_parser('grammars/book_grammars/drt.fcfg', trace=0, fstruct_reader=FeatStructReader(fdict_class=FeatStructNonterminal, logic_parser=DrtParser())) + >>> for tree in parser.parse('every girl chases a dog'.split()): + ... print(tree.label()['SEM'].simplify().normalize()) + ... + ([],[(([z1],[girl(z1)]) -> ([z2],[dog(z2), chase(z1,z2)]))]) + + + +Unit Tests +========== + +Parser +------ + + >>> print(dexpr(r'([x,y],[sees(x,y)])')) + ([x,y],[sees(x,y)]) + >>> print(dexpr(r'([x],[man(x), walks(x)])')) + ([x],[man(x), walks(x)]) + >>> print(dexpr(r'\x.([],[man(x), walks(x)])')) + \x.([],[man(x), walks(x)]) + >>> print(dexpr(r'\x.\y.([],[sees(x,y)])')) + \x y.([],[sees(x,y)]) + + >>> print(dexpr(r'([x,y],[(x = y)])')) + ([x,y],[(x = y)]) + >>> print(dexpr(r'([x,y],[(x != y)])')) + ([x,y],[-(x = y)]) + + >>> print(dexpr(r'\x.([],[walks(x)])(john)')) + (\x.([],[walks(x)]))(john) + >>> print(dexpr(r'\R.\x.([],[big(x,R)])(\y.([],[mouse(y)]))')) + (\R x.([],[big(x,R)]))(\y.([],[mouse(y)])) + + >>> print(dexpr(r'(([x],[walks(x)]) + ([y],[runs(y)]))')) + (([x],[walks(x)]) + ([y],[runs(y)])) + >>> print(dexpr(r'(([x,y],[walks(x), jumps(y)]) + (([z],[twos(z)]) + ([w],[runs(w)])))')) + (([x,y],[walks(x), jumps(y)]) + ([z],[twos(z)]) + ([w],[runs(w)])) + >>> print(dexpr(r'((([],[walks(x)]) + ([],[twos(x)])) + ([],[runs(x)]))')) + (([],[walks(x)]) + ([],[twos(x)]) + ([],[runs(x)])) + >>> print(dexpr(r'((([],[walks(x)]) + ([],[runs(x)])) + (([],[threes(x)]) + ([],[fours(x)])))')) + (([],[walks(x)]) + ([],[runs(x)]) + ([],[threes(x)]) + ([],[fours(x)])) + + >>> print(dexpr(r'(([],[walks(x)]) -> ([],[runs(x)]))')) + (([],[walks(x)]) -> ([],[runs(x)])) + + >>> print(dexpr(r'([x],[PRO(x), sees(John,x)])')) + ([x],[PRO(x), sees(John,x)]) + >>> print(dexpr(r'([x],[man(x), -([],[walks(x)])])')) + ([x],[man(x), -([],[walks(x)])]) + >>> print(dexpr(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])')) + ([],[(([x],[man(x)]) -> ([],[walks(x)]))]) + + >>> print(dexpr(r'DRS([x],[walk(x)])')) + ([x],[walk(x)]) + >>> print(dexpr(r'DRS([x][walk(x)])')) + ([x],[walk(x)]) + >>> print(dexpr(r'([x][walk(x)])')) + ([x],[walk(x)]) + +``simplify()`` +-------------- + + >>> print(dexpr(r'\x.([],[man(x), walks(x)])(john)').simplify()) + ([],[man(john), walks(john)]) + >>> print(dexpr(r'\x.\y.([z],[dog(z),sees(x,y)])(john)(mary)').simplify()) + ([z],[dog(z), sees(john,mary)]) + >>> print(dexpr(r'\R x.([],[big(x,R)])(\y.([],[mouse(y)]))').simplify()) + \x.([],[big(x,\y.([],[mouse(y)]))]) + + >>> print(dexpr(r'(([x],[walks(x)]) + ([y],[runs(y)]))').simplify()) + ([x,y],[walks(x), runs(y)]) + >>> print(dexpr(r'(([x,y],[walks(x), jumps(y)]) + (([z],[twos(z)]) + ([w],[runs(w)])))').simplify()) + ([w,x,y,z],[walks(x), jumps(y), twos(z), runs(w)]) + >>> print(dexpr(r'((([],[walks(x)]) + ([],[runs(x)]) + ([],[threes(x)]) + ([],[fours(x)])))').simplify()) + ([],[walks(x), runs(x), threes(x), fours(x)]) + >>> dexpr(r'([x],[man(x)])+([x],[walks(x)])').simplify() == \ + ... dexpr(r'([x,z1],[man(x), walks(z1)])') + True + >>> dexpr(r'([y],[boy(y), (([x],[dog(x)]) -> ([],[chase(x,y)]))])+([x],[run(x)])').simplify() == \ + ... dexpr(r'([y,z1],[boy(y), (([x],[dog(x)]) -> ([],[chase(x,y)])), run(z1)])') + True + + >>> dexpr(r'\Q.(([x],[john(x),walks(x)]) + Q)(([x],[PRO(x),leaves(x)]))').simplify() == \ + ... dexpr(r'([x,z1],[john(x), walks(x), PRO(z1), leaves(z1)])') + True + + >>> logic._counter._value = 0 + >>> print(dexpr('([],[(([x],[dog(x)]) -> ([e,y],[boy(y), chase(e), subj(e,x), obj(e,y)]))])+([e,x],[PRO(x), run(e), subj(e,x)])').simplify().normalize().normalize()) + ([e02,z5],[(([z3],[dog(z3)]) -> ([e01,z4],[boy(z4), chase(e01), subj(e01,z3), obj(e01,z4)])), PRO(z5), run(e02), subj(e02,z5)]) + +``fol()`` +----------- + + >>> print(dexpr(r'([x,y],[sees(x,y)])').fol()) + exists x y.sees(x,y) + >>> print(dexpr(r'([x],[man(x), walks(x)])').fol()) + exists x.(man(x) & walks(x)) + >>> print(dexpr(r'\x.([],[man(x), walks(x)])').fol()) + \x.(man(x) & walks(x)) + >>> print(dexpr(r'\x y.([],[sees(x,y)])').fol()) + \x y.sees(x,y) + + >>> print(dexpr(r'\x.([],[walks(x)])(john)').fol()) + \x.walks(x)(john) + >>> print(dexpr(r'\R x.([],[big(x,R)])(\y.([],[mouse(y)]))').fol()) + (\R x.big(x,R))(\y.mouse(y)) + + >>> print(dexpr(r'(([x],[walks(x)]) + ([y],[runs(y)]))').fol()) + (exists x.walks(x) & exists y.runs(y)) + + >>> print(dexpr(r'(([],[walks(x)]) -> ([],[runs(x)]))').fol()) + (walks(x) -> runs(x)) + + >>> print(dexpr(r'([x],[PRO(x), sees(John,x)])').fol()) + exists x.(PRO(x) & sees(John,x)) + >>> print(dexpr(r'([x],[man(x), -([],[walks(x)])])').fol()) + exists x.(man(x) & -walks(x)) + >>> print(dexpr(r'([],[(([x],[man(x)]) -> ([],[walks(x)]))])').fol()) + all x.(man(x) -> walks(x)) + + >>> print(dexpr(r'([x],[man(x) | walks(x)])').fol()) + exists x.(man(x) | walks(x)) + >>> print(dexpr(r'P(x) + ([x],[walks(x)])').fol()) + (P(x) & exists x.walks(x)) + +``resolve_anaphora()`` +---------------------- + + >>> from nltk.sem.drt import AnaphoraResolutionException + + >>> print(resolve_anaphora(dexpr(r'([x,y,z],[dog(x), cat(y), walks(z), PRO(z)])'))) + ([x,y,z],[dog(x), cat(y), walks(z), (z = [x,y])]) + >>> print(resolve_anaphora(dexpr(r'([],[(([x],[dog(x)]) -> ([y],[walks(y), PRO(y)]))])'))) + ([],[(([x],[dog(x)]) -> ([y],[walks(y), (y = x)]))]) + >>> print(resolve_anaphora(dexpr(r'(([x,y],[]) + ([],[PRO(x)]))')).simplify()) + ([x,y],[(x = y)]) + >>> try: print(resolve_anaphora(dexpr(r'([x],[walks(x), PRO(x)])'))) + ... except AnaphoraResolutionException as e: print(e) + Variable 'x' does not resolve to anything. + >>> print(resolve_anaphora(dexpr('([e01,z6,z7],[boy(z6), PRO(z7), run(e01), subj(e01,z7)])'))) + ([e01,z6,z7],[boy(z6), (z7 = z6), run(e01), subj(e01,z7)]) + +``equiv()``: +---------------- + + >>> a = dexpr(r'([x],[man(x), walks(x)])') + >>> b = dexpr(r'([x],[walks(x), man(x)])') + >>> print(a.equiv(b, TableauProver())) + True + + +``replace()``: +-------------- + + >>> a = dexpr(r'a') + >>> w = dexpr(r'w') + >>> x = dexpr(r'x') + >>> y = dexpr(r'y') + >>> z = dexpr(r'z') + + +replace bound +------------- + + >>> print(dexpr(r'([x],[give(x,y,z)])').replace(x.variable, a, False)) + ([x],[give(x,y,z)]) + >>> print(dexpr(r'([x],[give(x,y,z)])').replace(x.variable, a, True)) + ([a],[give(a,y,z)]) + +replace unbound +--------------- + + >>> print(dexpr(r'([x],[give(x,y,z)])').replace(y.variable, a, False)) + ([x],[give(x,a,z)]) + >>> print(dexpr(r'([x],[give(x,y,z)])').replace(y.variable, a, True)) + ([x],[give(x,a,z)]) + +replace unbound with bound +-------------------------- + + >>> dexpr(r'([x],[give(x,y,z)])').replace(y.variable, x, False) == \ + ... dexpr('([z1],[give(z1,x,z)])') + True + >>> dexpr(r'([x],[give(x,y,z)])').replace(y.variable, x, True) == \ + ... dexpr('([z1],[give(z1,x,z)])') + True + +replace unbound with unbound +---------------------------- + + >>> print(dexpr(r'([x],[give(x,y,z)])').replace(y.variable, z, False)) + ([x],[give(x,z,z)]) + >>> print(dexpr(r'([x],[give(x,y,z)])').replace(y.variable, z, True)) + ([x],[give(x,z,z)]) + + +replace unbound +--------------- + + >>> print(dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, False)) + (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) + >>> print(dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, True)) + (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) + +replace bound +------------- + + >>> print(dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(x.variable, a, False)) + (([x],[P(x,y,z)]) + ([y],[Q(x,y,z)])) + >>> print(dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(x.variable, a, True)) + (([a],[P(a,y,z)]) + ([y],[Q(a,y,z)])) + +replace unbound with unbound +---------------------------- + + >>> print(dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, False)) + (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) + >>> print(dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,z)])').replace(z.variable, a, True)) + (([x],[P(x,y,a)]) + ([y],[Q(x,y,a)])) + +replace unbound with bound on same side +--------------------------------------- + + >>> dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(z.variable, x, False) == \ + ... dexpr(r'(([z1],[P(z1,y,x)]) + ([y],[Q(z1,y,w)]))') + True + >>> dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(z.variable, x, True) == \ + ... dexpr(r'(([z1],[P(z1,y,x)]) + ([y],[Q(z1,y,w)]))') + True + +replace unbound with bound on other side +---------------------------------------- + + >>> dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(w.variable, x, False) == \ + ... dexpr(r'(([z1],[P(z1,y,z)]) + ([y],[Q(z1,y,x)]))') + True + >>> dexpr(r'([x],[P(x,y,z)])+([y],[Q(x,y,w)])').replace(w.variable, x, True) == \ + ... dexpr(r'(([z1],[P(z1,y,z)]) + ([y],[Q(z1,y,x)]))') + True + +replace unbound with double bound +--------------------------------- + + >>> dexpr(r'([x],[P(x,y,z)])+([x],[Q(x,y,w)])').replace(z.variable, x, False) == \ + ... dexpr(r'(([z1],[P(z1,y,x)]) + ([z1],[Q(z1,y,w)]))') + True + >>> dexpr(r'([x],[P(x,y,z)])+([x],[Q(x,y,w)])').replace(z.variable, x, True) == \ + ... dexpr(r'(([z1],[P(z1,y,x)]) + ([z1],[Q(z1,y,w)]))') + True + + +regression tests +---------------- + + >>> d = dexpr('([x],[A(c), ([y],[B(x,y,z,a)])->([z],[C(x,y,z,a)])])') + >>> print(d) + ([x],[A(c), (([y],[B(x,y,z,a)]) -> ([z],[C(x,y,z,a)]))]) + >>> print(d.pretty_format()) + ____________________________________ + | x | + |------------------------------------| + | A(c) | + | ____________ ____________ | + | | y | | z | | + | (|------------| -> |------------|) | + | | B(x,y,z,a) | | C(x,y,z,a) | | + | |____________| |____________| | + |____________________________________| + >>> print(str(d)) + ([x],[A(c), (([y],[B(x,y,z,a)]) -> ([z],[C(x,y,z,a)]))]) + >>> print(d.fol()) + exists x.(A(c) & all y.(B(x,y,z,a) -> exists z.C(x,y,z,a))) + >>> print(d.replace(Variable('a'), DrtVariableExpression(Variable('r')))) + ([x],[A(c), (([y],[B(x,y,z,r)]) -> ([z],[C(x,y,z,r)]))]) + >>> print(d.replace(Variable('x'), DrtVariableExpression(Variable('r')))) + ([x],[A(c), (([y],[B(x,y,z,a)]) -> ([z],[C(x,y,z,a)]))]) + >>> print(d.replace(Variable('y'), DrtVariableExpression(Variable('r')))) + ([x],[A(c), (([y],[B(x,y,z,a)]) -> ([z],[C(x,y,z,a)]))]) + >>> print(d.replace(Variable('z'), DrtVariableExpression(Variable('r')))) + ([x],[A(c), (([y],[B(x,y,r,a)]) -> ([z],[C(x,y,z,a)]))]) + >>> print(d.replace(Variable('x'), DrtVariableExpression(Variable('r')), True)) + ([r],[A(c), (([y],[B(r,y,z,a)]) -> ([z],[C(r,y,z,a)]))]) + >>> print(d.replace(Variable('y'), DrtVariableExpression(Variable('r')), True)) + ([x],[A(c), (([r],[B(x,r,z,a)]) -> ([z],[C(x,r,z,a)]))]) + >>> print(d.replace(Variable('z'), DrtVariableExpression(Variable('r')), True)) + ([x],[A(c), (([y],[B(x,y,r,a)]) -> ([r],[C(x,y,r,a)]))]) + >>> print(d == dexpr('([l],[A(c), ([m],[B(l,m,z,a)])->([n],[C(l,m,n,a)])])')) + True + >>> d = dexpr('([],[([x,y],[B(x,y,h), ([a,b],[dee(x,a,g)])])->([z,w],[cee(x,y,f), ([c,d],[E(x,c,d,e)])])])') + >>> sorted(d.free()) + [Variable('B'), Variable('E'), Variable('e'), Variable('f'), Variable('g'), Variable('h')] + >>> sorted(d.variables()) + [Variable('B'), Variable('E'), Variable('e'), Variable('f'), Variable('g'), Variable('h')] + >>> sorted(d.get_refs(True)) + [Variable('a'), Variable('b'), Variable('c'), Variable('d'), Variable('w'), Variable('x'), Variable('y'), Variable('z')] + >>> sorted(d.conds[0].get_refs(False)) + [Variable('x'), Variable('y')] + >>> print(dexpr('([x,y],[A(x,y), (x=y), ([],[B(x,y)])->([],[C(x,y)]), ([x,y],[D(x,y)])->([],[E(x,y)]), ([],[F(x,y)])->([x,y],[G(x,y)])])').eliminate_equality()) + ([x],[A(x,x), (([],[B(x,x)]) -> ([],[C(x,x)])), (([x,y],[D(x,y)]) -> ([],[E(x,y)])), (([],[F(x,x)]) -> ([x,y],[G(x,y)]))]) + >>> print(dexpr('([x,y],[A(x,y), (x=y)]) -> ([],[B(x,y)])').eliminate_equality()) + (([x],[A(x,x)]) -> ([],[B(x,x)])) + >>> print(dexpr('([x,y],[A(x,y)]) -> ([],[B(x,y), (x=y)])').eliminate_equality()) + (([x,y],[A(x,y)]) -> ([],[B(x,x)])) + >>> print(dexpr('([x,y],[A(x,y), (x=y), ([],[B(x,y)])])').eliminate_equality()) + ([x],[A(x,x), ([],[B(x,x)])]) + >>> print(dexpr('([x,y],[A(x,y), ([],[B(x,y), (x=y)])])').eliminate_equality()) + ([x,y],[A(x,y), ([],[B(x,x)])]) + >>> print(dexpr('([z8 z9 z10],[A(z8), z8=z10, z9=z10, B(z9), C(z10), D(z10)])').eliminate_equality()) + ([z9],[A(z9), B(z9), C(z9), D(z9)]) + + >>> print(dexpr('([x,y],[A(x,y), (x=y), ([],[B(x,y)]), ([x,y],[C(x,y)])])').eliminate_equality()) + ([x],[A(x,x), ([],[B(x,x)]), ([x,y],[C(x,y)])]) + >>> print(dexpr('([x,y],[A(x,y)]) + ([],[B(x,y), (x=y)]) + ([],[C(x,y)])').eliminate_equality()) + ([x],[A(x,x), B(x,x), C(x,x)]) + >>> print(dexpr('([x,y],[B(x,y)])+([x,y],[C(x,y)])').replace(Variable('y'), DrtVariableExpression(Variable('x')))) + (([x,y],[B(x,y)]) + ([x,y],[C(x,y)])) + >>> print(dexpr('(([x,y],[B(x,y)])+([],[C(x,y)]))+([],[D(x,y)])').replace(Variable('y'), DrtVariableExpression(Variable('x')))) + (([x,y],[B(x,y)]) + ([],[C(x,y)]) + ([],[D(x,y)])) + >>> print(dexpr('(([],[B(x,y)])+([],[C(x,y)]))+([],[D(x,y)])').replace(Variable('y'), DrtVariableExpression(Variable('x')))) + (([],[B(x,x)]) + ([],[C(x,x)]) + ([],[D(x,x)])) + >>> print(dexpr('(([],[B(x,y), ([x,y],[A(x,y)])])+([],[C(x,y)]))+([],[D(x,y)])').replace(Variable('y'), DrtVariableExpression(Variable('x'))).normalize()) + (([],[B(z3,z1), ([z2,z3],[A(z3,z2)])]) + ([],[C(z3,z1)]) + ([],[D(z3,z1)])) + + +Parse errors +============ + + >>> def parse_error(drtstring): + ... try: dexpr(drtstring) + ... except logic.LogicalExpressionException as e: print(e) + + >>> parse_error(r'') + End of input found. Expression expected. + + ^ + >>> parse_error(r'(') + End of input found. Expression expected. + ( + ^ + >>> parse_error(r'()') + Unexpected token: ')'. Expression expected. + () + ^ + >>> parse_error(r'([') + End of input found. Expected token ']'. + ([ + ^ + >>> parse_error(r'([,') + ',' is an illegal variable name. Constants may not be quantified. + ([, + ^ + >>> parse_error(r'([x,') + End of input found. Variable expected. + ([x, + ^ + >>> parse_error(r'([]') + End of input found. Expected token '['. + ([] + ^ + >>> parse_error(r'([][') + End of input found. Expected token ']'. + ([][ + ^ + >>> parse_error(r'([][,') + Unexpected token: ','. Expression expected. + ([][, + ^ + >>> parse_error(r'([][]') + End of input found. Expected token ')'. + ([][] + ^ + >>> parse_error(r'([x][man(x)]) |') + End of input found. Expression expected. + ([x][man(x)]) | + ^ + +Pretty Printing +=============== + + >>> dexpr(r"([],[])").pretty_print() + __ + | | + |--| + |__| + + >>> dexpr(r"([],[([x],[big(x), dog(x)]) -> ([],[bark(x)]) -([x],[walk(x)])])").pretty_print() + _____________________________ + | | + |-----------------------------| + | ________ _________ | + | | x | | | | + | (|--------| -> |---------|) | + | | big(x) | | bark(x) | | + | | dog(x) | |_________| | + | |________| | + | _________ | + | | x | | + | __ |---------| | + | | | walk(x) | | + | |_________| | + |_____________________________| + + >>> dexpr(r"([x,y],[x=y]) + ([z],[dog(z), walk(z)])").pretty_print() + _________ _________ + | x y | | z | + (|---------| + |---------|) + | (x = y) | | dog(z) | + |_________| | walk(z) | + |_________| + + >>> dexpr(r"([],[([x],[]) | ([y],[]) | ([z],[dog(z), walk(z)])])").pretty_print() + _______________________________ + | | + |-------------------------------| + | ___ ___ _________ | + | | x | | y | | z | | + | (|---| | |---| | |---------|) | + | |___| |___| | dog(z) | | + | | walk(z) | | + | |_________| | + |_______________________________| + + >>> dexpr(r"\P.\Q.(([x],[]) + P(x) + Q(x))(\x.([],[dog(x)]))").pretty_print() + ___ ________ + \ | x | \ | | + /\ P Q.(|---| + P(x) + Q(x))( /\ x.|--------|) + |___| | dog(x) | + |________| diff --git a/nltk/test/featgram.doctest b/nltk/test/featgram.doctest new file mode 100644 index 0000000..44d6328 --- /dev/null +++ b/nltk/test/featgram.doctest @@ -0,0 +1,610 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +========================= + Feature Grammar Parsing +========================= + +.. definitions from nltk_book/definitions.rst + +.. role:: feat + :class: feature +.. role:: fval + :class: fval +.. |rarr| unicode:: U+2192 .. right arrow +.. |dot| unicode:: U+2022 .. bullet +.. |pi| unicode:: U+03C0 + +Grammars can be parsed from strings. + + >>> import nltk + >>> from nltk import grammar, parse + >>> g = """ + ... % start DP + ... DP[AGR=?a] -> D[AGR=?a] N[AGR=?a] + ... D[AGR=[NUM='sg', PERS=3]] -> 'this' | 'that' + ... D[AGR=[NUM='pl', PERS=3]] -> 'these' | 'those' + ... D[AGR=[NUM='pl', PERS=1]] -> 'we' + ... D[AGR=[PERS=2]] -> 'you' + ... N[AGR=[NUM='sg', GND='m']] -> 'boy' + ... N[AGR=[NUM='pl', GND='m']] -> 'boys' + ... N[AGR=[NUM='sg', GND='f']] -> 'girl' + ... N[AGR=[NUM='pl', GND='f']] -> 'girls' + ... N[AGR=[NUM='sg']] -> 'student' + ... N[AGR=[NUM='pl']] -> 'students' + ... """ + >>> grammar = grammar.FeatureGrammar.fromstring(g) + >>> tokens = 'these girls'.split() + >>> parser = parse.FeatureEarleyChartParser(grammar) + >>> trees = parser.parse(tokens) + >>> for tree in trees: print(tree) + (DP[AGR=[GND='f', NUM='pl', PERS=3]] + (D[AGR=[NUM='pl', PERS=3]] these) + (N[AGR=[GND='f', NUM='pl']] girls)) + +In general, when we are trying to develop even a very small grammar, +it is convenient to put the rules in a file where they can be edited, +tested and revised. Let's assume that we have saved feat0cfg as a file named +``'feat0.fcfg'`` and placed it in the NLTK ``data`` directory. We can +inspect it as follows: + + >>> nltk.data.show_cfg('grammars/book_grammars/feat0.fcfg') + % start S + # ################### + # Grammar Productions + # ################### + # S expansion productions + S -> NP[NUM=?n] VP[NUM=?n] + # NP expansion productions + NP[NUM=?n] -> N[NUM=?n] + NP[NUM=?n] -> PropN[NUM=?n] + NP[NUM=?n] -> Det[NUM=?n] N[NUM=?n] + NP[NUM=pl] -> N[NUM=pl] + # VP expansion productions + VP[TENSE=?t, NUM=?n] -> IV[TENSE=?t, NUM=?n] + VP[TENSE=?t, NUM=?n] -> TV[TENSE=?t, NUM=?n] NP + # ################### + # Lexical Productions + # ################### + Det[NUM=sg] -> 'this' | 'every' + Det[NUM=pl] -> 'these' | 'all' + Det -> 'the' | 'some' | 'several' + PropN[NUM=sg]-> 'Kim' | 'Jody' + N[NUM=sg] -> 'dog' | 'girl' | 'car' | 'child' + N[NUM=pl] -> 'dogs' | 'girls' | 'cars' | 'children' + IV[TENSE=pres, NUM=sg] -> 'disappears' | 'walks' + TV[TENSE=pres, NUM=sg] -> 'sees' | 'likes' + IV[TENSE=pres, NUM=pl] -> 'disappear' | 'walk' + TV[TENSE=pres, NUM=pl] -> 'see' | 'like' + IV[TENSE=past] -> 'disappeared' | 'walked' + TV[TENSE=past] -> 'saw' | 'liked' + +Assuming we have saved feat0cfg as a file named +``'feat0.fcfg'``, the function ``parse.load_parser`` allows us to +read the grammar into NLTK, ready for use in parsing. + + + >>> cp = parse.load_parser('grammars/book_grammars/feat0.fcfg', trace=1) + >>> sent = 'Kim likes children' + >>> tokens = sent.split() + >>> tokens + ['Kim', 'likes', 'children'] + >>> trees = cp.parse(tokens) + |.Kim .like.chil.| + |[----] . .| [0:1] 'Kim' + |. [----] .| [1:2] 'likes' + |. . [----]| [2:3] 'children' + |[----] . .| [0:1] PropN[NUM='sg'] -> 'Kim' * + |[----] . .| [0:1] NP[NUM='sg'] -> PropN[NUM='sg'] * + |[----> . .| [0:1] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'sg'} + |. [----] .| [1:2] TV[NUM='sg', TENSE='pres'] -> 'likes' * + |. [----> .| [1:2] VP[NUM=?n, TENSE=?t] -> TV[NUM=?n, TENSE=?t] * NP[] {?n: 'sg', ?t: 'pres'} + |. . [----]| [2:3] N[NUM='pl'] -> 'children' * + |. . [----]| [2:3] NP[NUM='pl'] -> N[NUM='pl'] * + |. . [---->| [2:3] S[] -> NP[NUM=?n] * VP[NUM=?n] {?n: 'pl'} + |. [---------]| [1:3] VP[NUM='sg', TENSE='pres'] -> TV[NUM='sg', TENSE='pres'] NP[] * + |[==============]| [0:3] S[] -> NP[NUM='sg'] VP[NUM='sg'] * + >>> for tree in trees: print(tree) + (S[] + (NP[NUM='sg'] (PropN[NUM='sg'] Kim)) + (VP[NUM='sg', TENSE='pres'] + (TV[NUM='sg', TENSE='pres'] likes) + (NP[NUM='pl'] (N[NUM='pl'] children)))) + +The parser works directly with +the underspecified productions given by the grammar. That is, the +Predictor rule does not attempt to compile out all admissible feature +combinations before trying to expand the non-terminals on the left hand +side of a production. However, when the Scanner matches an input word +against a lexical production that has been predicted, the new edge will +typically contain fully specified features; e.g., the edge +[PropN[`num`:feat: = `sg`:fval:] |rarr| 'Kim', (0, 1)]. Recall from +Chapter 8 that the Fundamental (or Completer) Rule in +standard CFGs is used to combine an incomplete edge that's expecting a +nonterminal *B* with a following, complete edge whose left hand side +matches *B*. In our current setting, rather than checking for a +complete match, we test whether the expected category *B* will +unify with the left hand side *B'* of a following complete +edge. We will explain in more detail in Section 9.2 how +unification works; for the moment, it is enough to know that as a +result of unification, any variable values of features in *B* will be +instantiated by constant values in the corresponding feature structure +in *B'*, and these instantiated values will be used in the new edge +added by the Completer. This instantiation can be seen, for example, +in the edge +[NP [`num`:feat:\ =\ `sg`:fval:] |rarr| PropN[`num`:feat:\ =\ `sg`:fval:] |dot|, (0, 1)] +in Example 9.2, where the feature `num`:feat: has been assigned the value `sg`:fval:. + +Feature structures in NLTK are ... Atomic feature values can be strings or +integers. + + >>> fs1 = nltk.FeatStruct(TENSE='past', NUM='sg') + >>> print(fs1) + [ NUM = 'sg' ] + [ TENSE = 'past' ] + +We can think of a feature structure as being like a Python dictionary, +and access its values by indexing in the usual way. + + >>> fs1 = nltk.FeatStruct(PER=3, NUM='pl', GND='fem') + >>> print(fs1['GND']) + fem + +We can also define feature structures which have complex values, as +discussed earlier. + + >>> fs2 = nltk.FeatStruct(POS='N', AGR=fs1) + >>> print(fs2) + [ [ GND = 'fem' ] ] + [ AGR = [ NUM = 'pl' ] ] + [ [ PER = 3 ] ] + [ ] + [ POS = 'N' ] + >>> print(fs2['AGR']) + [ GND = 'fem' ] + [ NUM = 'pl' ] + [ PER = 3 ] + >>> print(fs2['AGR']['PER']) + 3 + +Feature structures can also be constructed using the ``parse()`` +method of the ``nltk.FeatStruct`` class. Note that in this case, atomic +feature values do not need to be enclosed in quotes. + + >>> f1 = nltk.FeatStruct("[NUMBER = sg]") + >>> f2 = nltk.FeatStruct("[PERSON = 3]") + >>> print(nltk.unify(f1, f2)) + [ NUMBER = 'sg' ] + [ PERSON = 3 ] + + >>> f1 = nltk.FeatStruct("[A = [B = b, D = d]]") + >>> f2 = nltk.FeatStruct("[A = [C = c, D = d]]") + >>> print(nltk.unify(f1, f2)) + [ [ B = 'b' ] ] + [ A = [ C = 'c' ] ] + [ [ D = 'd' ] ] + + +Feature Structures as Graphs +---------------------------- + +Feature structures are not inherently tied to linguistic objects; they are +general purpose structures for representing knowledge. For example, we +could encode information about a person in a feature structure: + + >>> person01 = nltk.FeatStruct("[NAME=Lee, TELNO='01 27 86 42 96',AGE=33]") + >>> print(person01) + [ AGE = 33 ] + [ NAME = 'Lee' ] + [ TELNO = '01 27 86 42 96' ] + +There are a number of notations for representing reentrancy in +matrix-style representations of feature structures. In NLTK, we adopt +the following convention: the first occurrence of a shared feature structure +is prefixed with an integer in parentheses, such as ``(1)``, and any +subsequent reference to that structure uses the notation +``->(1)``, as shown below. + + + >>> fs = nltk.FeatStruct("""[NAME=Lee, ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'], + ... SPOUSE=[NAME=Kim, ADDRESS->(1)]]""") + >>> print(fs) + [ ADDRESS = (1) [ NUMBER = 74 ] ] + [ [ STREET = 'rue Pascal' ] ] + [ ] + [ NAME = 'Lee' ] + [ ] + [ SPOUSE = [ ADDRESS -> (1) ] ] + [ [ NAME = 'Kim' ] ] + +There can be any number of tags within a single feature structure. + + >>> fs3 = nltk.FeatStruct("[A=(1)[B=b], C=(2)[], D->(1), E->(2)]") + >>> print(fs3) + [ A = (1) [ B = 'b' ] ] + [ ] + [ C = (2) [] ] + [ ] + [ D -> (1) ] + [ E -> (2) ] + >>> fs1 = nltk.FeatStruct(NUMBER=74, STREET='rue Pascal') + >>> fs2 = nltk.FeatStruct(CITY='Paris') + >>> print(nltk.unify(fs1, fs2)) + [ CITY = 'Paris' ] + [ NUMBER = 74 ] + [ STREET = 'rue Pascal' ] + +Unification is symmetric: + + >>> nltk.unify(fs1, fs2) == nltk.unify(fs2, fs1) + True + +Unification is commutative: + + >>> fs3 = nltk.FeatStruct(TELNO='01 27 86 42 96') + >>> nltk.unify(nltk.unify(fs1, fs2), fs3) == nltk.unify(fs1, nltk.unify(fs2, fs3)) + True + +Unification between *FS*:math:`_0` and *FS*:math:`_1` will fail if the +two feature structures share a path |pi|, +but the value of |pi| in *FS*:math:`_0` is a distinct +atom from the value of |pi| in *FS*:math:`_1`. In NLTK, +this is implemented by setting the result of unification to be +``None``. + + >>> fs0 = nltk.FeatStruct(A='a') + >>> fs1 = nltk.FeatStruct(A='b') + >>> print(nltk.unify(fs0, fs1)) + None + +Now, if we look at how unification interacts with structure-sharing, +things become really interesting. + + + + >>> fs0 = nltk.FeatStruct("""[NAME=Lee, + ... ADDRESS=[NUMBER=74, + ... STREET='rue Pascal'], + ... SPOUSE= [NAME=Kim, + ... ADDRESS=[NUMBER=74, + ... STREET='rue Pascal']]]""") + >>> print(fs0) + [ ADDRESS = [ NUMBER = 74 ] ] + [ [ STREET = 'rue Pascal' ] ] + [ ] + [ NAME = 'Lee' ] + [ ] + [ [ ADDRESS = [ NUMBER = 74 ] ] ] + [ SPOUSE = [ [ STREET = 'rue Pascal' ] ] ] + [ [ ] ] + [ [ NAME = 'Kim' ] ] + + + >>> fs1 = nltk.FeatStruct("[SPOUSE=[ADDRESS=[CITY=Paris]]]") + >>> print(nltk.unify(fs0, fs1)) + [ ADDRESS = [ NUMBER = 74 ] ] + [ [ STREET = 'rue Pascal' ] ] + [ ] + [ NAME = 'Lee' ] + [ ] + [ [ [ CITY = 'Paris' ] ] ] + [ [ ADDRESS = [ NUMBER = 74 ] ] ] + [ SPOUSE = [ [ STREET = 'rue Pascal' ] ] ] + [ [ ] ] + [ [ NAME = 'Kim' ] ] + + >>> fs2 = nltk.FeatStruct("""[NAME=Lee, ADDRESS=(1)[NUMBER=74, STREET='rue Pascal'], + ... SPOUSE=[NAME=Kim, ADDRESS->(1)]]""") + + + >>> print(fs2) + [ ADDRESS = (1) [ NUMBER = 74 ] ] + [ [ STREET = 'rue Pascal' ] ] + [ ] + [ NAME = 'Lee' ] + [ ] + [ SPOUSE = [ ADDRESS -> (1) ] ] + [ [ NAME = 'Kim' ] ] + + + >>> print(nltk.unify(fs2, fs1)) + [ [ CITY = 'Paris' ] ] + [ ADDRESS = (1) [ NUMBER = 74 ] ] + [ [ STREET = 'rue Pascal' ] ] + [ ] + [ NAME = 'Lee' ] + [ ] + [ SPOUSE = [ ADDRESS -> (1) ] ] + [ [ NAME = 'Kim' ] ] + + + >>> fs1 = nltk.FeatStruct("[ADDRESS1=[NUMBER=74, STREET='rue Pascal']]") + >>> fs2 = nltk.FeatStruct("[ADDRESS1=?x, ADDRESS2=?x]") + >>> print(fs2) + [ ADDRESS1 = ?x ] + [ ADDRESS2 = ?x ] + >>> print(nltk.unify(fs1, fs2)) + [ ADDRESS1 = (1) [ NUMBER = 74 ] ] + [ [ STREET = 'rue Pascal' ] ] + [ ] + [ ADDRESS2 -> (1) ] + + + + + >>> sent = 'who do you claim that you like' + >>> tokens = sent.split() + >>> cp = parse.load_parser('grammars/book_grammars/feat1.fcfg', trace=1) + >>> trees = cp.parse(tokens) + |.w.d.y.c.t.y.l.| + |[-] . . . . . .| [0:1] 'who' + |. [-] . . . . .| [1:2] 'do' + |. . [-] . . . .| [2:3] 'you' + |. . . [-] . . .| [3:4] 'claim' + |. . . . [-] . .| [4:5] 'that' + |. . . . . [-] .| [5:6] 'you' + |. . . . . . [-]| [6:7] 'like' + |# . . . . . . .| [0:0] NP[]/NP[] -> * + |. # . . . . . .| [1:1] NP[]/NP[] -> * + |. . # . . . . .| [2:2] NP[]/NP[] -> * + |. . . # . . . .| [3:3] NP[]/NP[] -> * + |. . . . # . . .| [4:4] NP[]/NP[] -> * + |. . . . . # . .| [5:5] NP[]/NP[] -> * + |. . . . . . # .| [6:6] NP[]/NP[] -> * + |. . . . . . . #| [7:7] NP[]/NP[] -> * + |[-] . . . . . .| [0:1] NP[+WH] -> 'who' * + |[-> . . . . . .| [0:1] S[-INV] -> NP[] * VP[] {} + |[-> . . . . . .| [0:1] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} + |[-> . . . . . .| [0:1] S[-INV] -> NP[] * S[]/NP[] {} + |. [-] . . . . .| [1:2] V[+AUX] -> 'do' * + |. [-> . . . . .| [1:2] S[+INV] -> V[+AUX] * NP[] VP[] {} + |. [-> . . . . .| [1:2] S[+INV]/?x[] -> V[+AUX] * NP[] VP[]/?x[] {} + |. [-> . . . . .| [1:2] VP[] -> V[+AUX] * VP[] {} + |. [-> . . . . .| [1:2] VP[]/?x[] -> V[+AUX] * VP[]/?x[] {} + |. . [-] . . . .| [2:3] NP[-WH] -> 'you' * + |. . [-> . . . .| [2:3] S[-INV] -> NP[] * VP[] {} + |. . [-> . . . .| [2:3] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} + |. . [-> . . . .| [2:3] S[-INV] -> NP[] * S[]/NP[] {} + |. [---> . . . .| [1:3] S[+INV] -> V[+AUX] NP[] * VP[] {} + |. [---> . . . .| [1:3] S[+INV]/?x[] -> V[+AUX] NP[] * VP[]/?x[] {} + |. . . [-] . . .| [3:4] V[-AUX, SUBCAT='clause'] -> 'claim' * + |. . . [-> . . .| [3:4] VP[] -> V[-AUX, SUBCAT='clause'] * SBar[] {} + |. . . [-> . . .| [3:4] VP[]/?x[] -> V[-AUX, SUBCAT='clause'] * SBar[]/?x[] {} + |. . . . [-] . .| [4:5] Comp[] -> 'that' * + |. . . . [-> . .| [4:5] SBar[] -> Comp[] * S[-INV] {} + |. . . . [-> . .| [4:5] SBar[]/?x[] -> Comp[] * S[-INV]/?x[] {} + |. . . . . [-] .| [5:6] NP[-WH] -> 'you' * + |. . . . . [-> .| [5:6] S[-INV] -> NP[] * VP[] {} + |. . . . . [-> .| [5:6] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} + |. . . . . [-> .| [5:6] S[-INV] -> NP[] * S[]/NP[] {} + |. . . . . . [-]| [6:7] V[-AUX, SUBCAT='trans'] -> 'like' * + |. . . . . . [->| [6:7] VP[] -> V[-AUX, SUBCAT='trans'] * NP[] {} + |. . . . . . [->| [6:7] VP[]/?x[] -> V[-AUX, SUBCAT='trans'] * NP[]/?x[] {} + |. . . . . . [-]| [6:7] VP[]/NP[] -> V[-AUX, SUBCAT='trans'] NP[]/NP[] * + |. . . . . [---]| [5:7] S[-INV]/NP[] -> NP[] VP[]/NP[] * + |. . . . [-----]| [4:7] SBar[]/NP[] -> Comp[] S[-INV]/NP[] * + |. . . [-------]| [3:7] VP[]/NP[] -> V[-AUX, SUBCAT='clause'] SBar[]/NP[] * + |. . [---------]| [2:7] S[-INV]/NP[] -> NP[] VP[]/NP[] * + |. [-----------]| [1:7] S[+INV]/NP[] -> V[+AUX] NP[] VP[]/NP[] * + |[=============]| [0:7] S[-INV] -> NP[] S[]/NP[] * + + >>> trees = list(trees) + >>> for tree in trees: print(tree) + (S[-INV] + (NP[+WH] who) + (S[+INV]/NP[] + (V[+AUX] do) + (NP[-WH] you) + (VP[]/NP[] + (V[-AUX, SUBCAT='clause'] claim) + (SBar[]/NP[] + (Comp[] that) + (S[-INV]/NP[] + (NP[-WH] you) + (VP[]/NP[] (V[-AUX, SUBCAT='trans'] like) (NP[]/NP[] ))))))) + +A different parser should give the same parse trees, but perhaps in a different order: + + >>> cp2 = parse.load_parser('grammars/book_grammars/feat1.fcfg', trace=1, + ... parser=parse.FeatureEarleyChartParser) + >>> trees2 = cp2.parse(tokens) + |.w.d.y.c.t.y.l.| + |[-] . . . . . .| [0:1] 'who' + |. [-] . . . . .| [1:2] 'do' + |. . [-] . . . .| [2:3] 'you' + |. . . [-] . . .| [3:4] 'claim' + |. . . . [-] . .| [4:5] 'that' + |. . . . . [-] .| [5:6] 'you' + |. . . . . . [-]| [6:7] 'like' + |> . . . . . . .| [0:0] S[-INV] -> * NP[] VP[] {} + |> . . . . . . .| [0:0] S[-INV]/?x[] -> * NP[] VP[]/?x[] {} + |> . . . . . . .| [0:0] S[-INV] -> * NP[] S[]/NP[] {} + |> . . . . . . .| [0:0] S[-INV] -> * Adv[+NEG] S[+INV] {} + |> . . . . . . .| [0:0] S[+INV] -> * V[+AUX] NP[] VP[] {} + |> . . . . . . .| [0:0] S[+INV]/?x[] -> * V[+AUX] NP[] VP[]/?x[] {} + |> . . . . . . .| [0:0] NP[+WH] -> * 'who' {} + |[-] . . . . . .| [0:1] NP[+WH] -> 'who' * + |[-> . . . . . .| [0:1] S[-INV] -> NP[] * VP[] {} + |[-> . . . . . .| [0:1] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} + |[-> . . . . . .| [0:1] S[-INV] -> NP[] * S[]/NP[] {} + |. > . . . . . .| [1:1] S[-INV]/?x[] -> * NP[] VP[]/?x[] {} + |. > . . . . . .| [1:1] S[+INV]/?x[] -> * V[+AUX] NP[] VP[]/?x[] {} + |. > . . . . . .| [1:1] V[+AUX] -> * 'do' {} + |. > . . . . . .| [1:1] VP[]/?x[] -> * V[-AUX, SUBCAT='trans'] NP[]/?x[] {} + |. > . . . . . .| [1:1] VP[]/?x[] -> * V[-AUX, SUBCAT='clause'] SBar[]/?x[] {} + |. > . . . . . .| [1:1] VP[]/?x[] -> * V[+AUX] VP[]/?x[] {} + |. > . . . . . .| [1:1] VP[] -> * V[-AUX, SUBCAT='intrans'] {} + |. > . . . . . .| [1:1] VP[] -> * V[-AUX, SUBCAT='trans'] NP[] {} + |. > . . . . . .| [1:1] VP[] -> * V[-AUX, SUBCAT='clause'] SBar[] {} + |. > . . . . . .| [1:1] VP[] -> * V[+AUX] VP[] {} + |. [-] . . . . .| [1:2] V[+AUX] -> 'do' * + |. [-> . . . . .| [1:2] S[+INV]/?x[] -> V[+AUX] * NP[] VP[]/?x[] {} + |. [-> . . . . .| [1:2] VP[]/?x[] -> V[+AUX] * VP[]/?x[] {} + |. [-> . . . . .| [1:2] VP[] -> V[+AUX] * VP[] {} + |. . > . . . . .| [2:2] VP[] -> * V[-AUX, SUBCAT='intrans'] {} + |. . > . . . . .| [2:2] VP[] -> * V[-AUX, SUBCAT='trans'] NP[] {} + |. . > . . . . .| [2:2] VP[] -> * V[-AUX, SUBCAT='clause'] SBar[] {} + |. . > . . . . .| [2:2] VP[] -> * V[+AUX] VP[] {} + |. . > . . . . .| [2:2] VP[]/?x[] -> * V[-AUX, SUBCAT='trans'] NP[]/?x[] {} + |. . > . . . . .| [2:2] VP[]/?x[] -> * V[-AUX, SUBCAT='clause'] SBar[]/?x[] {} + |. . > . . . . .| [2:2] VP[]/?x[] -> * V[+AUX] VP[]/?x[] {} + |. . > . . . . .| [2:2] NP[-WH] -> * 'you' {} + |. . [-] . . . .| [2:3] NP[-WH] -> 'you' * + |. [---> . . . .| [1:3] S[+INV]/?x[] -> V[+AUX] NP[] * VP[]/?x[] {} + |. . . > . . . .| [3:3] VP[]/?x[] -> * V[-AUX, SUBCAT='trans'] NP[]/?x[] {} + |. . . > . . . .| [3:3] VP[]/?x[] -> * V[-AUX, SUBCAT='clause'] SBar[]/?x[] {} + |. . . > . . . .| [3:3] VP[]/?x[] -> * V[+AUX] VP[]/?x[] {} + |. . . > . . . .| [3:3] V[-AUX, SUBCAT='clause'] -> * 'claim' {} + |. . . [-] . . .| [3:4] V[-AUX, SUBCAT='clause'] -> 'claim' * + |. . . [-> . . .| [3:4] VP[]/?x[] -> V[-AUX, SUBCAT='clause'] * SBar[]/?x[] {} + |. . . . > . . .| [4:4] SBar[]/?x[] -> * Comp[] S[-INV]/?x[] {} + |. . . . > . . .| [4:4] Comp[] -> * 'that' {} + |. . . . [-] . .| [4:5] Comp[] -> 'that' * + |. . . . [-> . .| [4:5] SBar[]/?x[] -> Comp[] * S[-INV]/?x[] {} + |. . . . . > . .| [5:5] S[-INV]/?x[] -> * NP[] VP[]/?x[] {} + |. . . . . > . .| [5:5] NP[-WH] -> * 'you' {} + |. . . . . [-] .| [5:6] NP[-WH] -> 'you' * + |. . . . . [-> .| [5:6] S[-INV]/?x[] -> NP[] * VP[]/?x[] {} + |. . . . . . > .| [6:6] VP[]/?x[] -> * V[-AUX, SUBCAT='trans'] NP[]/?x[] {} + |. . . . . . > .| [6:6] VP[]/?x[] -> * V[-AUX, SUBCAT='clause'] SBar[]/?x[] {} + |. . . . . . > .| [6:6] VP[]/?x[] -> * V[+AUX] VP[]/?x[] {} + |. . . . . . > .| [6:6] V[-AUX, SUBCAT='trans'] -> * 'like' {} + |. . . . . . [-]| [6:7] V[-AUX, SUBCAT='trans'] -> 'like' * + |. . . . . . [->| [6:7] VP[]/?x[] -> V[-AUX, SUBCAT='trans'] * NP[]/?x[] {} + |. . . . . . . #| [7:7] NP[]/NP[] -> * + |. . . . . . [-]| [6:7] VP[]/NP[] -> V[-AUX, SUBCAT='trans'] NP[]/NP[] * + |. . . . . [---]| [5:7] S[-INV]/NP[] -> NP[] VP[]/NP[] * + |. . . . [-----]| [4:7] SBar[]/NP[] -> Comp[] S[-INV]/NP[] * + |. . . [-------]| [3:7] VP[]/NP[] -> V[-AUX, SUBCAT='clause'] SBar[]/NP[] * + |. [-----------]| [1:7] S[+INV]/NP[] -> V[+AUX] NP[] VP[]/NP[] * + |[=============]| [0:7] S[-INV] -> NP[] S[]/NP[] * + + >>> sorted(trees) == sorted(trees2) + True + + +Let's load a German grammar: + + >>> cp = parse.load_parser('grammars/book_grammars/german.fcfg', trace=0) + >>> sent = 'die Katze sieht den Hund' + >>> tokens = sent.split() + >>> trees = cp.parse(tokens) + >>> for tree in trees: print(tree) + (S[] + (NP[AGR=[GND='fem', NUM='sg', PER=3], CASE='nom'] + (Det[AGR=[GND='fem', NUM='sg', PER=3], CASE='nom'] die) + (N[AGR=[GND='fem', NUM='sg', PER=3]] Katze)) + (VP[AGR=[NUM='sg', PER=3]] + (TV[AGR=[NUM='sg', PER=3], OBJCASE='acc'] sieht) + (NP[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc'] + (Det[AGR=[GND='masc', NUM='sg', PER=3], CASE='acc'] den) + (N[AGR=[GND='masc', NUM='sg', PER=3]] Hund)))) + +Grammar with Binding Operators +------------------------------ +The bindop.fcfg grammar is a semantic grammar that uses lambda +calculus. Each element has a core semantics, which is a single lambda +calculus expression; and a set of binding operators, which bind +variables. + +In order to make the binding operators work right, they need to +instantiate their bound variable every time they are added to the +chart. To do this, we use a special subclass of `Chart`, called +`InstantiateVarsChart`. + + >>> from nltk.parse.featurechart import InstantiateVarsChart + >>> cp = parse.load_parser('grammars/sample_grammars/bindop.fcfg', trace=1, + ... chart_class=InstantiateVarsChart) + >>> print(cp.grammar()) + Grammar with 15 productions (start state = S[]) + S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] VP[SEM=[BO=?b2, CORE=?vp]] + VP[SEM=[BO={?b1+?b2}, CORE=]] -> TV[SEM=[BO=?b1, CORE=?v]] NP[SEM=[BO=?b2, CORE=?obj]] + VP[SEM=?s] -> IV[SEM=?s] + NP[SEM=[BO={?b1+?b2+{bo(?det(?n),@x)}}, CORE=<@x>]] -> Det[SEM=[BO=?b1, CORE=?det]] N[SEM=[BO=?b2, CORE=?n]] + Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] -> 'a' + N[SEM=[BO={/}, CORE=]] -> 'dog' + N[SEM=[BO={/}, CORE=]] -> 'cat' + N[SEM=[BO={/}, CORE=]] -> 'mouse' + IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'barks' + IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'eats' + IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'walks' + TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] -> 'feeds' + TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] -> 'walks' + NP[SEM=[BO={bo(\P.P(John),@x)}, CORE=<@x>]] -> 'john' + NP[SEM=[BO={bo(\P.P(John),@x)}, CORE=<@x>]] -> 'alex' + +A simple intransitive sentence: + + >>> from nltk.sem import logic + >>> logic._counter._value = 100 + + >>> trees = cp.parse('john barks'.split()) + |. john.barks.| + |[-----] .| [0:1] 'john' + |. [-----]| [1:2] 'barks' + |[-----] .| [0:1] NP[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] -> 'john' * + |[-----> .| [0:1] S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] * VP[SEM=[BO=?b2, CORE=?vp]] {?b1: {bo(\P.P(John),z2)}, ?subj: } + |. [-----]| [1:2] IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> 'barks' * + |. [-----]| [1:2] VP[SEM=[BO={/}, CORE=<\x.bark(x)>]] -> IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] * + |[===========]| [0:2] S[SEM=[BO={bo(\P.P(John),z2)}, CORE=]] -> NP[SEM=[BO={bo(\P.P(John),z2)}, CORE=]] VP[SEM=[BO={/}, CORE=<\x.bark(x)>]] * + >>> for tree in trees: print(tree) + (S[SEM=[BO={bo(\P.P(John),z2)}, CORE=]] + (NP[SEM=[BO={bo(\P.P(John),z101)}, CORE=]] john) + (VP[SEM=[BO={/}, CORE=<\x.bark(x)>]] + (IV[SEM=[BO={/}, CORE=<\x.bark(x)>]] barks))) + +A transitive sentence: + + >>> trees = cp.parse('john feeds a dog'.split()) + |.joh.fee. a .dog.| + |[---] . . .| [0:1] 'john' + |. [---] . .| [1:2] 'feeds' + |. . [---] .| [2:3] 'a' + |. . . [---]| [3:4] 'dog' + |[---] . . .| [0:1] NP[SEM=[BO={bo(\P.P(John),z102)}, CORE=]] -> 'john' * + |[---> . . .| [0:1] S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] * VP[SEM=[BO=?b2, CORE=?vp]] {?b1: {bo(\P.P(John),z2)}, ?subj: } + |. [---] . .| [1:2] TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] -> 'feeds' * + |. [---> . .| [1:2] VP[SEM=[BO={?b1+?b2}, CORE=]] -> TV[SEM=[BO=?b1, CORE=?v]] * NP[SEM=[BO=?b2, CORE=?obj]] {?b1: {/}, ?v: } + |. . [---] .| [2:3] Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] -> 'a' * + |. . [---> .| [2:3] NP[SEM=[BO={?b1+?b2+{bo(?det(?n),@x)}}, CORE=<@x>]] -> Det[SEM=[BO=?b1, CORE=?det]] * N[SEM=[BO=?b2, CORE=?n]] {?b1: {/}, ?det: } + |. . . [---]| [3:4] N[SEM=[BO={/}, CORE=]] -> 'dog' * + |. . [-------]| [2:4] NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] -> Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] N[SEM=[BO={/}, CORE=]] * + |. . [------->| [2:4] S[SEM=[BO={?b1+?b2}, CORE=]] -> NP[SEM=[BO=?b1, CORE=?subj]] * VP[SEM=[BO=?b2, CORE=?vp]] {?b1: {bo(\P.exists x.(dog(x) & P(x)),z2)}, ?subj: } + |. [-----------]| [1:4] VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z2)}, CORE=<\y.feed(y,z2)>]] -> TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z2)}, CORE=]] * + |[===============]| [0:4] S[SEM=[BO={bo(\P.P(John),z2), bo(\P.exists x.(dog(x) & P(x)),z3)}, CORE=]] -> NP[SEM=[BO={bo(\P.P(John),z2)}, CORE=]] VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z3)}, CORE=<\y.feed(y,z3)>]] * + + >>> for tree in trees: print(tree) + (S[SEM=[BO={bo(\P.P(John),z2), bo(\P.exists x.(dog(x) & P(x)),z3)}, CORE=]] + (NP[SEM=[BO={bo(\P.P(John),z102)}, CORE=]] john) + (VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z2)}, CORE=<\y.feed(y,z2)>]] + (TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] feeds) + (NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z103)}, CORE=]] + (Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] a) + (N[SEM=[BO={/}, CORE=]] dog)))) + +Turn down the verbosity: + + >>> cp = parse.load_parser('grammars/sample_grammars/bindop.fcfg', trace=0, + ... chart_class=InstantiateVarsChart) + +Reuse the same lexical item twice: + + >>> trees = cp.parse('john feeds john'.split()) + >>> for tree in trees: print(tree) + (S[SEM=[BO={bo(\P.P(John),z2), bo(\P.P(John),z3)}, CORE=]] + (NP[SEM=[BO={bo(\P.P(John),z104)}, CORE=]] john) + (VP[SEM=[BO={bo(\P.P(John),z2)}, CORE=<\y.feed(y,z2)>]] + (TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] feeds) + (NP[SEM=[BO={bo(\P.P(John),z105)}, CORE=]] john))) + + >>> trees = cp.parse('a dog feeds a dog'.split()) + >>> for tree in trees: print(tree) + (S[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z2), bo(\P.exists x.(dog(x) & P(x)),z3)}, CORE=]] + (NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z106)}, CORE=]] + (Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] a) + (N[SEM=[BO={/}, CORE=]] dog)) + (VP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z2)}, CORE=<\y.feed(y,z2)>]] + (TV[SEM=[BO={/}, CORE=<\x y.feed(y,x)>]] feeds) + (NP[SEM=[BO={bo(\P.exists x.(dog(x) & P(x)),z107)}, CORE=]] + (Det[SEM=[BO={/}, CORE=<\Q P.exists x.(Q(x) & P(x))>]] a) + (N[SEM=[BO={/}, CORE=]] dog)))) diff --git a/nltk/test/featstruct.doctest b/nltk/test/featstruct.doctest new file mode 100644 index 0000000..54e7191 --- /dev/null +++ b/nltk/test/featstruct.doctest @@ -0,0 +1,1229 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +================================== + Feature Structures & Unification +================================== + >>> from nltk.featstruct import FeatStruct + >>> from nltk.sem.logic import Variable, VariableExpression, Expression + +.. note:: For now, featstruct uses the older lambdalogic semantics + module. Eventually, it should be updated to use the new first + order predicate logic module. + +Overview +~~~~~~~~ +A feature structure is a mapping from feature identifiers to feature +values, where feature values can be simple values (like strings or +ints), nested feature structures, or variables: + + >>> fs1 = FeatStruct(number='singular', person=3) + >>> print(fs1) + [ number = 'singular' ] + [ person = 3 ] + +Feature structure may be nested: + + >>> fs2 = FeatStruct(type='NP', agr=fs1) + >>> print(fs2) + [ agr = [ number = 'singular' ] ] + [ [ person = 3 ] ] + [ ] + [ type = 'NP' ] + +Variables are used to indicate that two features should be assigned +the same value. For example, the following feature structure requires +that the feature fs3['agr']['number'] be bound to the same value as the +feature fs3['subj']['number']. + + >>> fs3 = FeatStruct(agr=FeatStruct(number=Variable('?n')), + ... subj=FeatStruct(number=Variable('?n'))) + >>> print(fs3) + [ agr = [ number = ?n ] ] + [ ] + [ subj = [ number = ?n ] ] + +Feature structures are typically used to represent partial information +about objects. A feature name that is not mapped to a value stands +for a feature whose value is unknown (*not* a feature without a +value). Two feature structures that represent (potentially +overlapping) information about the same object can be combined by +*unification*. + + >>> print(fs2.unify(fs3)) + [ agr = [ number = 'singular' ] ] + [ [ person = 3 ] ] + [ ] + [ subj = [ number = 'singular' ] ] + [ ] + [ type = 'NP' ] + +When two inconsistent feature structures are unified, the unification +fails and returns ``None``. + + >>> fs4 = FeatStruct(agr=FeatStruct(person=1)) + >>> print(fs4.unify(fs2)) + None + >>> print(fs2.unify(fs4)) + None + +.. + >>> del fs1, fs2, fs3, fs4 # clean-up + +Feature Structure Types +----------------------- +There are actually two types of feature structure: + +- *feature dictionaries*, implemented by `FeatDict`, act like + Python dictionaries. Feature identifiers may be strings or + instances of the `Feature` class. +- *feature lists*, implemented by `FeatList`, act like Python + lists. Feature identifiers are integers. + +When you construct a feature structure using the `FeatStruct` +constructor, it will automatically decide which type is appropriate: + + >>> type(FeatStruct(number='singular')) + + >>> type(FeatStruct([1,2,3])) + + +Usually, we will just use feature dictionaries; but sometimes feature +lists can be useful too. Two feature lists will unify with each other +only if they have equal lengths, and all of their feature values +match. If you wish to write a feature list that contains 'unknown' +values, you must use variables: + + >>> fs1 = FeatStruct([1,2,Variable('?y')]) + >>> fs2 = FeatStruct([1,Variable('?x'),3]) + >>> fs1.unify(fs2) + [1, 2, 3] + +.. + >>> del fs1, fs2 # clean-up + +Parsing Feature Structure Strings +--------------------------------- +Feature structures can be constructed directly from strings. Often, +this is more convenient than constructing them directly. NLTK can +parse most feature strings to produce the corresponding feature +structures. (But you must restrict your base feature values to +strings, ints, logic expressions (`nltk.sem.logic.Expression`), and a +few other types discussed below). + +Feature dictionaries are written like Python dictionaries, except that +keys are not put in quotes; and square brackets (``[]``) are used +instead of braces (``{}``): + + >>> FeatStruct('[tense="past", agr=[number="sing", person=3]]') + [agr=[number='sing', person=3], tense='past'] + +If a feature value is a single alphanumeric word, then it does not +need to be quoted -- it will be automatically treated as a string: + + >>> FeatStruct('[tense=past, agr=[number=sing, person=3]]') + [agr=[number='sing', person=3], tense='past'] + +Feature lists are written like python lists: + + >>> FeatStruct('[1, 2, 3]') + [1, 2, 3] + +The expression ``[]`` is treated as an empty feature dictionary, not +an empty feature list: + + >>> type(FeatStruct('[]')) + + +Feature Paths +------------- +Features can be specified using *feature paths*, or tuples of feature +identifiers that specify path through the nested feature structures to +a value. + + >>> fs1 = FeatStruct('[x=1, y=[1,2,[z=3]]]') + >>> fs1['y'] + [1, 2, [z=3]] + >>> fs1['y', 2] + [z=3] + >>> fs1['y', 2, 'z'] + 3 + +.. + >>> del fs1 # clean-up + +Reentrance +---------- +Feature structures may contain reentrant feature values. A *reentrant +feature value* is a single feature structure that can be accessed via +multiple feature paths. + + >>> fs1 = FeatStruct(x='val') + >>> fs2 = FeatStruct(a=fs1, b=fs1) + >>> print(fs2) + [ a = (1) [ x = 'val' ] ] + [ ] + [ b -> (1) ] + >>> fs2 + [a=(1)[x='val'], b->(1)] + +As you can see, reentrane is displayed by marking a feature structure +with a unique identifier, in this case ``(1)``, the first time it is +encountered; and then using the special form ``var -> id`` whenever it +is encountered again. You can use the same notation to directly +create reentrant feature structures from strings. + + >>> FeatStruct('[a=(1)[], b->(1), c=[d->(1)]]') + [a=(1)[], b->(1), c=[d->(1)]] + +Reentrant feature structures may contain cycles: + + >>> fs3 = FeatStruct('(1)[a->(1)]') + >>> fs3['a', 'a', 'a', 'a'] + (1)[a->(1)] + >>> fs3['a', 'a', 'a', 'a'] is fs3 + True + +Unification preserves the reentrance relations imposed by both of the +unified feature structures. In the feature structure resulting from +unification, any modifications to a reentrant feature value will be +visible using any of its feature paths. + + >>> fs3.unify(FeatStruct('[a=[b=12], c=33]')) + (1)[a->(1), b=12, c=33] + +.. + >>> del fs1, fs2, fs3 # clean-up + +Feature Structure Equality +-------------------------- +Two feature structures are considered equal if they assign the same +values to all features, *and* they contain the same reentrances. + + >>> fs1 = FeatStruct('[a=(1)[x=1], b->(1)]') + >>> fs2 = FeatStruct('[a=(1)[x=1], b->(1)]') + >>> fs3 = FeatStruct('[a=[x=1], b=[x=1]]') + >>> fs1 == fs1, fs1 is fs1 + (True, True) + >>> fs1 == fs2, fs1 is fs2 + (True, False) + >>> fs1 == fs3, fs1 is fs3 + (False, False) + +Note that this differs from how Python dictionaries and lists define +equality -- in particular, Python dictionaries and lists ignore +reentrance relations. To test two feature structures for equality +while ignoring reentrance relations, use the `equal_values()` method: + + >>> fs1.equal_values(fs1) + True + >>> fs1.equal_values(fs2) + True + >>> fs1.equal_values(fs3) + True + +.. + >>> del fs1, fs2, fs3 # clean-up + +Feature Value Sets & Feature Value Tuples +----------------------------------------- +`nltk.featstruct` defines two new data types that are intended to be +used as feature values: `FeatureValueTuple` and `FeatureValueSet`. +Both of these types are considered base values -- i.e., unification +does *not* apply to them. However, variable binding *does* apply to +any values that they contain. + +Feature value tuples are written with parentheses: + + >>> fs1 = FeatStruct('[x=(?x, ?y)]') + >>> fs1 + [x=(?x, ?y)] + >>> fs1.substitute_bindings({Variable('?x'): 1, Variable('?y'): 2}) + [x=(1, 2)] + +Feature sets are written with braces: + + >>> fs1 = FeatStruct('[x={?x, ?y}]') + >>> fs1 + [x={?x, ?y}] + >>> fs1.substitute_bindings({Variable('?x'): 1, Variable('?y'): 2}) + [x={1, 2}] + +In addition to the basic feature value tuple & set classes, nltk +defines feature value unions (for sets) and feature value +concatenations (for tuples). These are written using '+', and can be +used to combine sets & tuples: + + >>> fs1 = FeatStruct('[x=((1, 2)+?z), z=?z]') + >>> fs1 + [x=((1, 2)+?z), z=?z] + >>> fs1.unify(FeatStruct('[z=(3, 4, 5)]')) + [x=(1, 2, 3, 4, 5), z=(3, 4, 5)] + +Thus, feature value tuples and sets can be used to build up tuples +and sets of values over the course of unification. For example, when +parsing sentences using a semantic feature grammar, feature sets or +feature tuples can be used to build a list of semantic predicates as +the sentence is parsed. + +As was mentioned above, unification does not apply to feature value +tuples and sets. One reason for this that it's impossible to define a +single correct answer for unification when concatenation is used. +Consider the following example: + + >>> fs1 = FeatStruct('[x=(1, 2, 3, 4)]') + >>> fs2 = FeatStruct('[x=(?a+?b), a=?a, b=?b]') + +If unification applied to feature tuples, then the unification +algorithm would have to arbitrarily choose how to divide the tuple +(1,2,3,4) into two parts. Instead, the unification algorithm refuses +to make this decision, and simply unifies based on value. Because +(1,2,3,4) is not equal to (?a+?b), fs1 and fs2 will not unify: + + >>> print(fs1.unify(fs2)) + None + +If you need a list-like structure that unification does apply to, use +`FeatList`. + +.. + >>> del fs1, fs2 # clean-up + +Light-weight Feature Structures +------------------------------- +Many of the functions defined by `nltk.featstruct` can be applied +directly to simple Python dictionaries and lists, rather than to +full-fledged `FeatDict` and `FeatList` objects. In other words, +Python ``dicts`` and ``lists`` can be used as "light-weight" feature +structures. + + >>> # Note: pprint prints dicts sorted + >>> from pprint import pprint + >>> from nltk.featstruct import unify + >>> pprint(unify(dict(x=1, y=dict()), dict(a='a', y=dict(b='b')))) + {'a': 'a', 'x': 1, 'y': {'b': 'b'}} + +However, you should keep in mind the following caveats: + +- Python dictionaries & lists ignore reentrance when checking for + equality between values. But two FeatStructs with different + reentrances are considered nonequal, even if all their base + values are equal. + +- FeatStructs can be easily frozen, allowing them to be used as + keys in hash tables. Python dictionaries and lists can not. + +- FeatStructs display reentrance in their string representations; + Python dictionaries and lists do not. + +- FeatStructs may *not* be mixed with Python dictionaries and lists + (e.g., when performing unification). + +- FeatStructs provide a number of useful methods, such as `walk()` + and `cyclic()`, which are not available for Python dicts & lists. + +In general, if your feature structures will contain any reentrances, +or if you plan to use them as dictionary keys, it is strongly +recommended that you use full-fledged `FeatStruct` objects. + +Custom Feature Values +--------------------- +The abstract base class `CustomFeatureValue` can be used to define new +base value types that have custom unification methods. For example, +the following feature value type encodes a range, and defines +unification as taking the intersection on the ranges: + + >>> from functools import total_ordering + >>> from nltk.featstruct import CustomFeatureValue, UnificationFailure + >>> @total_ordering + ... class Range(CustomFeatureValue): + ... def __init__(self, low, high): + ... assert low <= high + ... self.low = low + ... self.high = high + ... def unify(self, other): + ... if not isinstance(other, Range): + ... return UnificationFailure + ... low = max(self.low, other.low) + ... high = min(self.high, other.high) + ... if low <= high: return Range(low, high) + ... else: return UnificationFailure + ... def __repr__(self): + ... return '(%s>> fs1 = FeatStruct(x=Range(5,8), y=FeatStruct(z=Range(7,22))) + >>> print(fs1.unify(FeatStruct(x=Range(6, 22)))) + [ x = (6>> print(fs1.unify(FeatStruct(x=Range(9, 12)))) + None + >>> print(fs1.unify(FeatStruct(x=12))) + None + >>> print(fs1.unify(FeatStruct('[x=?x, y=[z=?x]]'))) + [ x = (7>> fs1 = FeatStruct(a=1, b=2, c=3) + >>> fs2 = FeatStruct(x=fs1, y='x') + +Feature structures support all dictionary methods (excluding the class +method `dict.fromkeys()`). Non-mutating methods: + + >>> sorted(fs2.keys()) # keys() + ['x', 'y'] + >>> sorted(fs2.values()) # values() + [[a=1, b=2, c=3], 'x'] + >>> sorted(fs2.items()) # items() + [('x', [a=1, b=2, c=3]), ('y', 'x')] + >>> sorted(fs2) # __iter__() + ['x', 'y'] + >>> 'a' in fs2, 'x' in fs2 # __contains__() + (False, True) + >>> fs2.has_key('a'), fs2.has_key('x') # has_key() + (False, True) + >>> fs2['x'], fs2['y'] # __getitem__() + ([a=1, b=2, c=3], 'x') + >>> fs2['a'] # __getitem__() + Traceback (most recent call last): + . . . + KeyError: 'a' + >>> fs2.get('x'), fs2.get('y'), fs2.get('a') # get() + ([a=1, b=2, c=3], 'x', None) + >>> fs2.get('x', 'hello'), fs2.get('a', 'hello') # get() + ([a=1, b=2, c=3], 'hello') + >>> len(fs1), len(fs2) # __len__ + (3, 2) + >>> fs2.copy() # copy() + [x=[a=1, b=2, c=3], y='x'] + >>> fs2.copy() is fs2 # copy() + False + +Note: by default, `FeatStruct.copy()` does a deep copy. Use +`FeatStruct.copy(deep=False)` for a shallow copy. + +.. + >>> del fs1, fs2 # clean-up. + +Dictionary access methods (mutating) +------------------------------------ + >>> fs1 = FeatStruct(a=1, b=2, c=3) + >>> fs2 = FeatStruct(x=fs1, y='x') + +Setting features (`__setitem__()`) + + >>> fs1['c'] = 5 + >>> fs1 + [a=1, b=2, c=5] + >>> fs1['x'] = 12 + >>> fs1 + [a=1, b=2, c=5, x=12] + >>> fs2['x', 'a'] = 2 + >>> fs2 + [x=[a=2, b=2, c=5, x=12], y='x'] + >>> fs1 + [a=2, b=2, c=5, x=12] + +Deleting features (`__delitem__()`) + + >>> del fs1['x'] + >>> fs1 + [a=2, b=2, c=5] + >>> del fs2['x', 'a'] + >>> fs1 + [b=2, c=5] + +`setdefault()`: + + >>> fs1.setdefault('b', 99) + 2 + >>> fs1 + [b=2, c=5] + >>> fs1.setdefault('x', 99) + 99 + >>> fs1 + [b=2, c=5, x=99] + +`update()`: + + >>> fs2.update({'a':'A', 'b':'B'}, c='C') + >>> fs2 + [a='A', b='B', c='C', x=[b=2, c=5, x=99], y='x'] + +`pop()`: + + >>> fs2.pop('a') + 'A' + >>> fs2 + [b='B', c='C', x=[b=2, c=5, x=99], y='x'] + >>> fs2.pop('a') + Traceback (most recent call last): + . . . + KeyError: 'a' + >>> fs2.pop('a', 'foo') + 'foo' + >>> fs2 + [b='B', c='C', x=[b=2, c=5, x=99], y='x'] + +`clear()`: + + >>> fs1.clear() + >>> fs1 + [] + >>> fs2 + [b='B', c='C', x=[], y='x'] + +`popitem()`: + + >>> sorted([fs2.popitem() for i in range(len(fs2))]) + [('b', 'B'), ('c', 'C'), ('x', []), ('y', 'x')] + >>> fs2 + [] + +Once a feature structure has been frozen, it may not be mutated. + + >>> fs1 = FeatStruct('[x=1, y=2, z=[a=3]]') + >>> fs1.freeze() + >>> fs1.frozen() + True + >>> fs1['z'].frozen() + True + + >>> fs1['x'] = 5 + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + >>> del fs1['x'] + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + >>> fs1.clear() + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + >>> fs1.pop('x') + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + >>> fs1.popitem() + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + >>> fs1.setdefault('x') + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + >>> fs1.update(z=22) + Traceback (most recent call last): + . . . + ValueError: Frozen FeatStructs may not be modified. + +.. + >>> del fs1, fs2 # clean-up. + +Feature Paths +------------- +Make sure that __getitem__ with feature paths works as intended: + + >>> fs1 = FeatStruct(a=1, b=2, + ... c=FeatStruct( + ... d=FeatStruct(e=12), + ... f=FeatStruct(g=55, h='hello'))) + >>> fs1[()] + [a=1, b=2, c=[d=[e=12], f=[g=55, h='hello']]] + >>> fs1['a'], fs1[('a',)] + (1, 1) + >>> fs1['c','d','e'] + 12 + >>> fs1['c','f','g'] + 55 + +Feature paths that select unknown features raise KeyError: + + >>> fs1['c', 'f', 'e'] + Traceback (most recent call last): + . . . + KeyError: ('c', 'f', 'e') + >>> fs1['q', 'p'] + Traceback (most recent call last): + . . . + KeyError: ('q', 'p') + +Feature paths that try to go 'through' a feature that's not a feature +structure raise KeyError: + + >>> fs1['a', 'b'] + Traceback (most recent call last): + . . . + KeyError: ('a', 'b') + +Feature paths can go through reentrant structures: + + >>> fs2 = FeatStruct('(1)[a=[b=[c->(1), d=5], e=11]]') + >>> fs2['a', 'b', 'c', 'a', 'e'] + 11 + >>> fs2['a', 'b', 'c', 'a', 'b', 'd'] + 5 + >>> fs2[tuple('abcabcabcabcabcabcabcabcabcabca')] + (1)[b=[c=[a->(1)], d=5], e=11] + +Indexing requires strings, `Feature`\s, or tuples; other types raise a +TypeError: + + >>> fs2[12] + Traceback (most recent call last): + . . . + TypeError: Expected feature name or path. Got 12. + >>> fs2[list('abc')] + Traceback (most recent call last): + . . . + TypeError: Expected feature name or path. Got ['a', 'b', 'c']. + +Feature paths can also be used with `get()`, `has_key()`, and +`__contains__()`. + + >>> fpath1 = tuple('abcabc') + >>> fpath2 = tuple('abcabz') + >>> fs2.get(fpath1), fs2.get(fpath2) + ((1)[a=[b=[c->(1), d=5], e=11]], None) + >>> fpath1 in fs2, fpath2 in fs2 + (True, False) + >>> fs2.has_key(fpath1), fs2.has_key(fpath2) + (True, False) + +.. + >>> del fs1, fs2 # clean-up + +Reading Feature Structures +-------------------------- + +Empty feature struct: + + >>> FeatStruct('[]') + [] + +Test features with integer values: + + >>> FeatStruct('[a=12, b=-33, c=0]') + [a=12, b=-33, c=0] + +Test features with string values. Either single or double quotes may +be used. Strings are evaluated just like python strings -- in +particular, you can use escape sequences and 'u' and 'r' prefixes, and +triple-quoted strings. + + >>> FeatStruct('[a="", b="hello", c="\'", d=\'\', e=\'"\']') + [a='', b='hello', c="'", d='', e='"'] + >>> FeatStruct(r'[a="\\", b="\"", c="\x6f\\y", d="12"]') + [a='\\', b='"', c='o\\y', d='12'] + >>> FeatStruct(r'[b=r"a\b\c"]') + [b='a\\b\\c'] + >>> FeatStruct('[x="""a"""]') + [x='a'] + +Test parsing of reentrant feature structures. + + >>> FeatStruct('[a=(1)[], b->(1)]') + [a=(1)[], b->(1)] + >>> FeatStruct('[a=(1)[x=1, y=2], b->(1)]') + [a=(1)[x=1, y=2], b->(1)] + +Test parsing of cyclic feature structures. + + >>> FeatStruct('[a=(1)[b->(1)]]') + [a=(1)[b->(1)]] + >>> FeatStruct('(1)[a=[b=[c->(1)]]]') + (1)[a=[b=[c->(1)]]] + +Strings of the form "+name" and "-name" may be used to specify boolean +values. + + >>> FeatStruct('[-bar, +baz, +foo]') + [-bar, +baz, +foo] + +None, True, and False are recognized as values: + + >>> FeatStruct('[bar=True, baz=False, foo=None]') + [+bar, -baz, foo=None] + +Special features: + + >>> FeatStruct('NP/VP') + NP[]/VP[] + >>> FeatStruct('?x/?x') + ?x[]/?x[] + >>> print(FeatStruct('VP[+fin, agr=?x, tense=past]/NP[+pl, agr=?x]')) + [ *type* = 'VP' ] + [ ] + [ [ *type* = 'NP' ] ] + [ *slash* = [ agr = ?x ] ] + [ [ pl = True ] ] + [ ] + [ agr = ?x ] + [ fin = True ] + [ tense = 'past' ] + +Here the slash feature gets coerced: + + >>> FeatStruct('[*slash*=a, x=b, *type*="NP"]') + NP[x='b']/a[] + + >>> FeatStruct('NP[sem=]/NP') + NP[sem=]/NP[] + >>> FeatStruct('S[sem=]') + S[sem=] + >>> print(FeatStruct('NP[sem=]/NP')) + [ *type* = 'NP' ] + [ ] + [ *slash* = [ *type* = 'NP' ] ] + [ ] + [ sem = ] + +Playing with ranges: + + >>> from nltk.featstruct import RangeFeature, FeatStructReader + >>> width = RangeFeature('width') + >>> reader = FeatStructReader([width]) + >>> fs1 = reader.fromstring('[*width*=-5:12]') + >>> fs2 = reader.fromstring('[*width*=2:123]') + >>> fs3 = reader.fromstring('[*width*=-7:-2]') + >>> fs1.unify(fs2) + [*width*=(2, 12)] + >>> fs1.unify(fs3) + [*width*=(-5, -2)] + >>> print(fs2.unify(fs3)) # no overlap in width. + None + +The slash feature has a default value of 'False': + + >>> print(FeatStruct('NP[]/VP').unify(FeatStruct('NP[]'), trace=1)) + + Unification trace: + / NP[]/VP[] + |\ NP[] + | + | Unify feature: *type* + | / 'NP' + | |\ 'NP' + | | + | +-->'NP' + | + | Unify feature: *slash* + | / VP[] + | |\ False + | | + X X <-- FAIL + None + +The demo structures from category.py. They all parse, but they don't +do quite the right thing, -- ?x vs x. + + >>> FeatStruct(pos='n', agr=FeatStruct(number='pl', gender='f')) + [agr=[gender='f', number='pl'], pos='n'] + >>> FeatStruct(r'NP[sem=]/NP') + NP[sem=]/NP[] + >>> FeatStruct(r'S[sem=]') + S[sem=] + >>> FeatStruct('?x/?x') + ?x[]/?x[] + >>> FeatStruct('VP[+fin, agr=?x, tense=past]/NP[+pl, agr=?x]') + VP[agr=?x, +fin, tense='past']/NP[agr=?x, +pl] + >>> FeatStruct('S[sem = ]') + S[sem=] + + >>> FeatStruct('S') + S[] + +The parser also includes support for reading sets and tuples. + + >>> FeatStruct('[x={1,2,2,2}, y={/}]') + [x={1, 2}, y={/}] + >>> FeatStruct('[x=(1,2,2,2), y=()]') + [x=(1, 2, 2, 2), y=()] + >>> print(FeatStruct('[x=(1,[z=(1,2,?x)],?z,{/})]')) + [ x = (1, [ z = (1, 2, ?x) ], ?z, {/}) ] + +Note that we can't put a featstruct inside a tuple, because doing so +would hash it, and it's not frozen yet: + + >>> print(FeatStruct('[x={[]}]')) + Traceback (most recent call last): + . . . + TypeError: FeatStructs must be frozen before they can be hashed. + +There's a special syntax for taking the union of sets: "{...+...}". +The elements should only be variables or sets. + + >>> FeatStruct('[x={?a+?b+{1,2,3}}]') + [x={?a+?b+{1, 2, 3}}] + +There's a special syntax for taking the concatenation of tuples: +"(...+...)". The elements should only be variables or tuples. + + >>> FeatStruct('[x=(?a+?b+(1,2,3))]') + [x=(?a+?b+(1, 2, 3))] + +Parsing gives helpful messages if your string contains an error. + + >>> FeatStruct('[a=, b=5]]') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + [a=, b=5]] + ^ Expected value + >>> FeatStruct('[a=12 22, b=33]') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + [a=12 22, b=33] + ^ Expected comma + >>> FeatStruct('[a=5] [b=6]') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + [a=5] [b=6] + ^ Expected end of string + >>> FeatStruct(' *++*') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + *++* + ^ Expected open bracket or identifier + >>> FeatStruct('[x->(1)]') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + [x->(1)] + ^ Expected bound identifier + >>> FeatStruct('[x->y]') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + [x->y] + ^ Expected identifier + >>> FeatStruct('') + Traceback (most recent call last): + . . . + ValueError: Error parsing feature structure + + ^ Expected open bracket or identifier + + +Unification +----------- +Very simple unifications give the expected results: + + >>> FeatStruct().unify(FeatStruct()) + [] + >>> FeatStruct(number='singular').unify(FeatStruct()) + [number='singular'] + >>> FeatStruct().unify(FeatStruct(number='singular')) + [number='singular'] + >>> FeatStruct(number='singular').unify(FeatStruct(person=3)) + [number='singular', person=3] + +Merging nested structures: + + >>> fs1 = FeatStruct('[A=[B=b]]') + >>> fs2 = FeatStruct('[A=[C=c]]') + >>> fs1.unify(fs2) + [A=[B='b', C='c']] + >>> fs2.unify(fs1) + [A=[B='b', C='c']] + +A basic case of reentrant unification + + >>> fs4 = FeatStruct('[A=(1)[B=b], E=[F->(1)]]') + >>> fs5 = FeatStruct("[A=[C='c'], E=[F=[D='d']]]") + >>> fs4.unify(fs5) + [A=(1)[B='b', C='c', D='d'], E=[F->(1)]] + >>> fs5.unify(fs4) + [A=(1)[B='b', C='c', D='d'], E=[F->(1)]] + +More than 2 paths to a value + + >>> fs1 = FeatStruct("[a=[],b=[],c=[],d=[]]") + >>> fs2 = FeatStruct('[a=(1)[], b->(1), c->(1), d->(1)]') + >>> fs1.unify(fs2) + [a=(1)[], b->(1), c->(1), d->(1)] + +fs1[a] gets unified with itself + + >>> fs1 = FeatStruct('[x=(1)[], y->(1)]') + >>> fs2 = FeatStruct('[x=(1)[], y->(1)]') + >>> fs1.unify(fs2) + [x=(1)[], y->(1)] + +Bound variables should get forwarded appropriately + + >>> fs1 = FeatStruct('[A=(1)[X=x], B->(1), C=?cvar, D=?dvar]') + >>> fs2 = FeatStruct('[A=(1)[Y=y], B=(2)[Z=z], C->(1), D->(2)]') + >>> fs1.unify(fs2) + [A=(1)[X='x', Y='y', Z='z'], B->(1), C->(1), D->(1)] + >>> fs2.unify(fs1) + [A=(1)[X='x', Y='y', Z='z'], B->(1), C->(1), D->(1)] + +Cyclic structure created by unification. + + >>> fs1 = FeatStruct('[F=(1)[], G->(1)]') + >>> fs2 = FeatStruct('[F=[H=(2)[]], G->(2)]') + >>> fs3 = fs1.unify(fs2) + >>> fs3 + [F=(1)[H->(1)], G->(1)] + >>> fs3['F'] is fs3['G'] + True + >>> fs3['F'] is fs3['G']['H'] + True + >>> fs3['F'] is fs3['G']['H']['H'] + True + >>> fs3['F'] is fs3['F']['H']['H']['H']['H']['H']['H']['H']['H'] + True + +Cyclic structure created w/ variables. + + >>> fs1 = FeatStruct('[F=[H=?x]]') + >>> fs2 = FeatStruct('[F=?x]') + >>> fs3 = fs1.unify(fs2, rename_vars=False) + >>> fs3 + [F=(1)[H->(1)]] + >>> fs3['F'] is fs3['F']['H'] + True + >>> fs3['F'] is fs3['F']['H']['H'] + True + >>> fs3['F'] is fs3['F']['H']['H']['H']['H']['H']['H']['H']['H'] + True + +Unifying w/ a cyclic feature structure. + + >>> fs4 = FeatStruct('[F=[H=[H=[H=(1)[]]]], K->(1)]') + >>> fs3.unify(fs4) + [F=(1)[H->(1)], K->(1)] + >>> fs4.unify(fs3) + [F=(1)[H->(1)], K->(1)] + +Variable bindings should preserve reentrance. + + >>> bindings = {} + >>> fs1 = FeatStruct("[a=?x]") + >>> fs2 = fs1.unify(FeatStruct("[a=[]]"), bindings) + >>> fs2['a'] is bindings[Variable('?x')] + True + >>> fs2.unify(FeatStruct("[b=?x]"), bindings) + [a=(1)[], b->(1)] + +Aliased variable tests + + >>> fs1 = FeatStruct("[a=?x, b=?x]") + >>> fs2 = FeatStruct("[b=?y, c=?y]") + >>> bindings = {} + >>> fs3 = fs1.unify(fs2, bindings) + >>> fs3 + [a=?x, b=?x, c=?x] + >>> bindings + {Variable('?y'): Variable('?x')} + >>> fs3.unify(FeatStruct("[a=1]")) + [a=1, b=1, c=1] + +If we keep track of the bindings, then we can use the same variable +over multiple calls to unify. + + >>> bindings = {} + >>> fs1 = FeatStruct('[a=?x]') + >>> fs2 = fs1.unify(FeatStruct('[a=[]]'), bindings) + >>> fs2.unify(FeatStruct('[b=?x]'), bindings) + [a=(1)[], b->(1)] + >>> bindings + {Variable('?x'): []} + +.. + >>> del fs1, fs2, fs3, fs4, fs5 # clean-up + +Unification Bindings +-------------------- + + >>> bindings = {} + >>> fs1 = FeatStruct('[a=?x]') + >>> fs2 = FeatStruct('[a=12]') + >>> fs3 = FeatStruct('[b=?x]') + >>> fs1.unify(fs2, bindings) + [a=12] + >>> bindings + {Variable('?x'): 12} + >>> fs3.substitute_bindings(bindings) + [b=12] + >>> fs3 # substitute_bindings didn't mutate fs3. + [b=?x] + >>> fs2.unify(fs3, bindings) + [a=12, b=12] + + >>> bindings = {} + >>> fs1 = FeatStruct('[a=?x, b=1]') + >>> fs2 = FeatStruct('[a=5, b=?x]') + >>> fs1.unify(fs2, bindings) + [a=5, b=1] + >>> sorted(bindings.items()) + [(Variable('?x'), 5), (Variable('?x2'), 1)] + +.. + >>> del fs1, fs2, fs3 # clean-up + +Expressions +----------- + + >>> e = Expression.fromstring('\\P y.P(z,y)') + >>> fs1 = FeatStruct(x=e, y=Variable('z')) + >>> fs2 = FeatStruct(y=VariableExpression(Variable('John'))) + >>> fs1.unify(fs2) + [x=<\P y.P(John,y)>, y=] + +Remove Variables +---------------- + + >>> FeatStruct('[a=?x, b=12, c=[d=?y]]').remove_variables() + [b=12, c=[]] + >>> FeatStruct('(1)[a=[b=?x,c->(1)]]').remove_variables() + (1)[a=[c->(1)]] + +Equality & Hashing +------------------ +The `equal_values` method checks whether two feature structures assign +the same value to every feature. If the optional argument +``check_reentrances`` is supplied, then it also returns false if there +is any difference in the reentrances. + + >>> a = FeatStruct('(1)[x->(1)]') + >>> b = FeatStruct('(1)[x->(1)]') + >>> c = FeatStruct('(1)[x=[x->(1)]]') + >>> d = FeatStruct('[x=(1)[x->(1)]]') + >>> e = FeatStruct('(1)[x=[x->(1), y=1], y=1]') + >>> def compare(x,y): + ... assert x.equal_values(y, True) == y.equal_values(x, True) + ... assert x.equal_values(y, False) == y.equal_values(x, False) + ... if x.equal_values(y, True): + ... assert x.equal_values(y, False) + ... print('equal values, same reentrance') + ... elif x.equal_values(y, False): + ... print('equal values, different reentrance') + ... else: + ... print('different values') + + >>> compare(a, a) + equal values, same reentrance + >>> compare(a, b) + equal values, same reentrance + >>> compare(a, c) + equal values, different reentrance + >>> compare(a, d) + equal values, different reentrance + >>> compare(c, d) + equal values, different reentrance + >>> compare(a, e) + different values + >>> compare(c, e) + different values + >>> compare(d, e) + different values + >>> compare(e, e) + equal values, same reentrance + +Feature structures may not be hashed until they are frozen: + + >>> hash(a) + Traceback (most recent call last): + . . . + TypeError: FeatStructs must be frozen before they can be hashed. + >>> a.freeze() + >>> v = hash(a) + +Feature structures define hash consistently. The following example +looks at the hash value for each (fs1,fs2) pair; if their hash values +are not equal, then they must not be equal. If their hash values are +equal, then display a message, and indicate whether their values are +indeed equal. Note that c and d currently have the same hash value, +even though they are not equal. That is not a bug, strictly speaking, +but it wouldn't be a bad thing if it changed. + + >>> for fstruct in (a, b, c, d, e): + ... fstruct.freeze() + >>> for fs1_name in 'abcde': + ... for fs2_name in 'abcde': + ... fs1 = locals()[fs1_name] + ... fs2 = locals()[fs2_name] + ... if hash(fs1) != hash(fs2): + ... assert fs1 != fs2 + ... else: + ... print('%s and %s have the same hash value,' % + ... (fs1_name, fs2_name)) + ... if fs1 == fs2: print('and are equal') + ... else: print('and are not equal') + a and a have the same hash value, and are equal + a and b have the same hash value, and are equal + b and a have the same hash value, and are equal + b and b have the same hash value, and are equal + c and c have the same hash value, and are equal + c and d have the same hash value, and are not equal + d and c have the same hash value, and are not equal + d and d have the same hash value, and are equal + e and e have the same hash value, and are equal + +.. + >>> del a, b, c, d, e, v # clean-up + +Tracing +------- + + >>> fs1 = FeatStruct('[a=[b=(1)[], c=?x], d->(1), e=[f=?x]]') + >>> fs2 = FeatStruct('[a=(1)[c="C"], e=[g->(1)]]') + >>> fs1.unify(fs2, trace=True) + + Unification trace: + / [a=[b=(1)[], c=?x], d->(1), e=[f=?x]] + |\ [a=(1)[c='C'], e=[g->(1)]] + | + | Unify feature: a + | / [b=[], c=?x] + | |\ [c='C'] + | | + | | Unify feature: a.c + | | / ?x + | | |\ 'C' + | | | + | | +-->Variable('?x') + | | + | +-->[b=[], c=?x] + | Bindings: {?x: 'C'} + | + | Unify feature: e + | / [f=?x] + | |\ [g=[c='C']] + | | + | +-->[f=?x, g=[b=[], c=?x]] + | Bindings: {?x: 'C'} + | + +-->[a=(1)[b=(2)[], c='C'], d->(2), e=[f='C', g->(1)]] + Bindings: {?x: 'C'} + [a=(1)[b=(2)[], c='C'], d->(2), e=[f='C', g->(1)]] + >>> + >>> fs1 = FeatStruct('[a=?x, b=?z, c=?z]') + >>> fs2 = FeatStruct('[a=?y, b=?y, c=?q]') + >>> #fs1.unify(fs2, trace=True) + >>> + +.. + >>> del fs1, fs2 # clean-up + +Unification on Dicts & Lists +---------------------------- +It's possible to do unification on dictionaries: + + >>> from nltk.featstruct import unify + >>> pprint(unify(dict(x=1, y=dict(z=2)), dict(x=1, q=5)), width=1) + {'q': 5, 'x': 1, 'y': {'z': 2}} + +It's possible to do unification on lists as well: + + >>> unify([1, 2, 3], [1, Variable('x'), 3]) + [1, 2, 3] + +Mixing dicts and lists is fine: + + >>> pprint(unify([dict(x=1, y=dict(z=2)),3], [dict(x=1, q=5),3]), + ... width=1) + [{'q': 5, 'x': 1, 'y': {'z': 2}}, 3] + +Mixing dicts and FeatStructs is discouraged: + + >>> unify(dict(x=1), FeatStruct(x=1)) + Traceback (most recent call last): + . . . + ValueError: Mixing FeatStruct objects with Python dicts and lists is not supported. + +But you can do it if you really want, by explicitly stating that both +dictionaries and FeatStructs should be treated as feature structures: + + >>> unify(dict(x=1), FeatStruct(x=1), fs_class=(dict, FeatStruct)) + {'x': 1} + +Finding Conflicts +----------------- + + >>> from nltk.featstruct import conflicts + >>> fs1 = FeatStruct('[a=[b=(1)[c=2], d->(1), e=[f->(1)]]]') + >>> fs2 = FeatStruct('[a=[b=[c=[x=5]], d=[c=2], e=[f=[c=3]]]]') + >>> for path in conflicts(fs1, fs2): + ... print('%-8s: %r vs %r' % ('.'.join(path), fs1[path], fs2[path])) + a.b.c : 2 vs [x=5] + a.e.f.c : 2 vs 3 + +.. + >>> del fs1, fs2 # clean-up + +Retracting Bindings +------------------- + + >>> from nltk.featstruct import retract_bindings + >>> bindings = {} + >>> fs1 = FeatStruct('[a=?x, b=[c=?y]]') + >>> fs2 = FeatStruct('[a=(1)[c=[d=1]], b->(1)]') + >>> fs3 = fs1.unify(fs2, bindings) + >>> print(fs3) + [ a = (1) [ c = [ d = 1 ] ] ] + [ ] + [ b -> (1) ] + >>> pprint(bindings) + {Variable('?x'): [c=[d=1]], Variable('?y'): [d=1]} + >>> retract_bindings(fs3, bindings) + [a=?x, b=?x] + >>> pprint(bindings) + {Variable('?x'): [c=?y], Variable('?y'): [d=1]} + +Squashed Bugs +~~~~~~~~~~~~~ +In svn rev 5167, unifying two feature structures that used the same +variable would cause those variables to become aliased in the output. + + >>> fs1 = FeatStruct('[a=?x]') + >>> fs2 = FeatStruct('[b=?x]') + >>> fs1.unify(fs2) + [a=?x, b=?x2] + +There was a bug in svn revision 5172 that caused `rename_variables` to +rename variables to names that are already used. + + >>> FeatStruct('[a=?x, b=?x2]').rename_variables( + ... vars=[Variable('?x')]) + [a=?x3, b=?x2] + >>> fs1 = FeatStruct('[a=?x]') + >>> fs2 = FeatStruct('[a=?x, b=?x2]') + >>> fs1.unify(fs2) + [a=?x, b=?x2] + +There was a bug in svn rev 5167 that caused us to get the following +example wrong. Basically the problem was that we only followed +'forward' pointers for other, not self, when unifying two feature +structures. (nb: this test assumes that features are unified in +alphabetical order -- if they are not, it might pass even if the bug +is present.) + + >>> fs1 = FeatStruct('[a=[x=1], b=?x, c=?x]') + >>> fs2 = FeatStruct('[a=(1)[], b->(1), c=[x=2]]') + >>> print(fs1.unify(fs2)) + None + +.. + >>> del fs1, fs2 # clean-up diff --git a/nltk/test/floresta.txt b/nltk/test/floresta.txt new file mode 100644 index 0000000..caa23ae --- /dev/null +++ b/nltk/test/floresta.txt @@ -0,0 +1,7924 @@ +O 7 e Meio um ex-libris da noite algarvia. + uma das mais antigas discotecas do Algarve, situada em Albufeira, que continua a manter os traos decorativos e as clientelas de sempre. + um pouco a verso de uma espcie de outro lado da noite, a meio caminho entre os devaneios de uma fauna perifrica, seja de Lisboa, Londres, Dublin ou Faro e Portimo, e a postura circunspecta dos fiis da casa, que dela esperam a msica geracionista dos 60 ou dos 70. +No deixa de ser, nos tempos que correm, um certo very typical algarvio, cabea de cartaz para os que querem fugir a algumas movimentaes nocturnas j a caminho da ritualizao de massas, do gnero vamos todos ao Calypso e encontramo-nos na Locomia. +E assim, aos 2,5 milhes que o Ministrio do Planeamento e Administrao do Territrio j gasta no pagamento do pessoal afecto a estes organismos, vm juntar-se os montantes das obras propriamente ditas, que os municpios, j com projectos na mo, vm reivindicar junto do Executivo, como salienta aquele membro do Governo. +E o dinheiro no falta s s cmaras, lembra o secretrio de Estado, que considera que a soluo para as autarquias especializarem-se em fundos comunitrios. +Mas como, se muitas no dispem, nos seus quadros, dos tcnicos necessrios? +Encomendem-nos a projectistas de fora porque, se as obras vierem a ser financiadas, eles at saem de graa, j que, nesse caso, os fundos comunitrios pagam os projectos, o mesmo no acontecendo quando eles so feitos pelos GAT, dado serem organismos do Estado. +Essa poder vir a ser uma hiptese, at porque, no terreno, a capacidade dos GAT est cada vez mais enfraquecida. +Alguns at j desapareceram, como o de Castro Verde, e outros tm vindo a perder quadros. +O primeiro fabricante mundial de ratos para computador, a empresa sua Logitech, apresentou esta semana numa feira especializada que teve lugar em Basileia (Sua) um equipamento perifrico denominado Audioman que permitir dotar os computadores de orelhas. +Segundo a empresa, o aparelho permite que o aparelho grave e transcreva a voz humana. +Estamos a dotar os computadores de um novo sentido disse Steve d'Averio, director de marketing para a Europa da Logitech. +O Audioman foi desenvolvido na Sua em apenas sete meses e compe-se de um microfone e de um altifalante que se podem acoplar facilmente a um computador, devendo ser comercializado ao preo de 290 francos suos (28 contos). +Junqueiro foi ainda confrontado com o facto de no ter falado com o ministro antes de avanar com a proposta. +Joo Cravinho, que integra a comitiva de Jorge Sampaio na visita de Estado a Moambique, ainda no reagiu carta que o dirigente socialista lhe enviou. +No estou a ver que, para emitir uma opinio, ns tivssemos de informar previamente o ministro, afirmou. +O senhor ministro interpretar esta sugesto como entender. +Junqueiro recordou ainda que, nas ltimas autrquicas, o IGAT suspendeu as suas actividades um ms antes das eleies. +Alm do Museu do Ar, o projecto gira em torno do parque temtico propriamente dito. +A zona ldica, com os divertimentos, reas comerciais de souvenirs e de restaurao, compreende espaos distintos para os vrios temas, ainda em anlise, tais como Portugal, Japo, Brasil, frica e Far-West. +Os quatro primeiros temas destinam-se a mostrar o papel de Portugal no mundo e o quinto, o nico sem relao com a histria nacional, justificado pela experincia de Barcelona (Port Aventura), que regista assinalvel sucesso. +Uma zona petting anexa, em cooperao com o Jardim Zoolgico de Lisboa, destina-se a permitir o contacto das crianas com animais. +Tudo, claro est, muito arborizado. +O museu, a desenvolver sob orientao de uma comisso de notveis, dirigida pelo Presidente da Repblica, est orado em seis milhes de contos, valor incomportvel para a Fora Area. +Da que a Cameron Hall tenha cado do cu. +A proposta muito bem vista, porque ser mais vantajosa do que se houver s um plo de interesse no local, afirmou o major Carlos Barbosa, das relaes pblicas da Fora Area, admitindo que, com o parque temtico, se o interesse for diversificado, toda a gente fica a ganhar. +A pouco mais de um ms do lanamento nacional do Rendimento Mnimo Garantido (RMG), o nmero de famlias j abrangidas pelos projectos-piloto deste programa de apoio aos agregados mais desfavorecidos no pra de aumentar. +7.777 famlias, totalizando 26.668 pessoas, esto j a usufruir do rendimento destinado a garantir condies consideradas mnimas de sobrevivncia e reinsero de cidados excludos socialmente. +O balano -- a que o PBLICO teve acesso -- traado pela comisso revela que o nmero de pessoas abrangidas pelo RMG aumentou 36 por cento relativamente ao ltimo balano de 30 de Maro. +Este crescimento resulta da opo de alargar o nmero de projectos-piloto, de modo a cobrir uma parte do territrio nacional at ao dia 1 de Julho, referiu ao PBLICO o presidente da Comisso Nacional do RMG, Paulo Pedroso. +Para tal, referiu aquele responsvel, foram montadas mais estruturas, mais zonas esto abrangidas e, por isso, mais pessoas se podem candidatar. +E tantos foram os candidatos que o perodo destinado a testar a aplicao do RMG acabaria por ceder lugar a um processo efectivo de financiamento. +A instituio deste direito s ser, contudo, efectivado depois do lanamento nacional do projecto dentro de pouco mais de um ms. +Mais metafrico foi o secretrio de Estado do Desenvolvimento Regional, Adriano Pimpo, que comparou o acordo a uma embraiagem. +Porque a embraiagem que pe o motor em contacto com as rodas que geram o movimento, que para ns o desenvolvimento. +Para que no surjam avarias, Pimpo pediu aos presentes que se empenhem na execuo dos termos do acordo, sob pena de a embraiagem se transformar em travo. +A propsito, no Museu da Segunda Guerra Mundial, que a foi aberto, a histria da maior guerra no continente europeu comea com a fotografia de Estaline a cumprimentar o ministro dos Negcios Estrangeiros da Alemanha nazi, ou seja, a guerra comea com a assinatura do Pacto Molotov-Ribbentrop. +Na cerimnia de inaugurao do edifcio, Ieltsin declarou perante mais de cem mil pessoas que a Rssia est perto da estabilidade poltica e que todos os problemas podem ser resolvidos mesa das conversaes. +Talvez entusiasmado pela festa da vitria, o Presidente russo afirmou que chegar o dia em que a Rssia ajudar o Ocidente. +As cerimnias oficiais terminaram com desfiles militares e recriaes de combates areos entre os aviadores soviticos e alemes. +Co-produo franco-egpcia, O Emigrante inspira-se na histria de Jos, filho de Jacob, contando o percurso de Ram que, h 3000 anos, decide abandonar a sua terra rida para se instalar no Egipto dos faras, centro da civilizao. +Tomando a defesa do filme de Chahine, numa sala atulhada, o bastonrio dos advogados, Ahmed al-Khawaga, replicou que o realizador egpcio se inspirou na histria de Jos, mas no se afastou das palavras do Coro que evoca, em termos claros, as propostas feitas ao profeta pela esposa do mestre que o comprou chegada ao Egipto. +Eu no sou francs para falar da cultura francesa, mas sei que ela deu uma contribuio importante cultura egpcia. +E uma vez que um dos nossos artistas conclui um acordo com um artista francs, isso no nos desonra. +O ltimo romance de Paul Auster -- que ainda no est traduzido. +Nashe encontra Pozzi, um jogador, com quem inicia um pquer extravagante. +No imenso desacerto que foi a defesa do Penafiel, o capito Vasco foi o homem que ainda segurou as pontas. +Seguro, eficiente, decidido -- tivesse o Penafiel outro Vasco e provavelmente o resultado teria sido outro. +VALTINHO -- Foi dos melhores jogadores do Penafiel, este brasileiro de pernas altas. +Dos seus ps ainda nasceu alguma coisa, embora o resto da equipa no ajudasse grande coisa. +Lutou como lhe vemos poucas vezes e ainda teve nos ps uma boa oportunidade de golo, mas teve que rematar em jeito e no em fora, como gosta mais. +(H quem defenda, no entanto, que se trata de um fax apcrifo, realmente escrito pelo deputado Jos Magalhes, o qual teria, alis, imitado a letra do ex-deputado Antnio Barreto. +Fontes fidedignas -- que o mesmo dizer, no jornalsticas -- garantiram entretanto que, entre as frases atribudas ao senhor primeiro-ministro que, de facto, lhe no pertencem esto o comentrio ao congresso Portugal e o futuro, a resposta ao deputado Adriano Moreira quando este o interrogou sobre a sua concepo de federalismo e -- at! -- a contestada frase sobre a fidedignidade dos jornalistas. +O Benfica voltou ontem a vencer o Lokomotiv de Moscovo pela diferena mnima (3-2), passando aos quartos-de-final da Taa das Taas em futebol. +O jogo chegou a estar complicado para a equipa de Paulo Autuori, essencialmente porque o Lokomotiv marcou muito cedo (8 ') e os jogadores portugueses no conseguiam adaptar-se ao estado do relvado. +Mas a expulso prematura e estpida de Gurenko, quando ainda no estava decorrida meia hora de jogo, seguida, pouco depois, da entrada de Panduru, foi o suficiente para dar a volta ao resultado. +O Benfica apresentou o seu esquema habitual, com Jamir e Tahar mais recuados e insistindo com a utilizao de Valdo na esquerda. +Em contrapartida, o Lokomotiv actuava com trs centrais, com o lbero Chugainov, como habitualmente, a comandar o jogo e o lateral-esquerdo Solomatin muito activo. +Muito bem estavam tambm os trs mdios -- Maminov, Drozdov e Kharlachev --, desempenhando Kosolapov as funes de pivot do ataque. +Por isso, e tambm porque os russos estavam mais bem adaptados ao muito enlameado e escorregadio relvado, o Lokomotiv era mais rpido sobre a bola, ganhando a maior parte dos duelos na zona fulcral do terreno, situada entre o meio-campo e a defesa do Benfica. +O cocktail que Jupp condena bem real. +Mas -o apenas porque este Governo e esta maioria, escolheram, desde Agosto do ano passado, a imigrao e a poltica da imigrao como ponto de clivagem poltica para as prximas legislativas, ontem mesmo marcadas para Maro de 1998, em simultneo com as regionais. +Um endurecimento ntido existe desde ento neste terreno altamente perigoso. +E razo desta escolha , obviamente, a progresso demente da Frente Nacional, que prospera sempre a apontar o imigrante como bode expiatrio e simultaneamente como a fonte de todos males do povo francs. +Foi com estupefaco e surpresa que li, na edio do passado fim-de-semana, os comunicados da direco e da administrao deste jornal. +Sou um leitor assduo e atento do PBLICO, desde o primeiro nmero, e no poucas vezes tenho manifestado opinies nas suas pginas, o que me leva agora a emitir o meu pensamento sobre o jornal, sobre quem o faz e sobre os ditos comunicados. +1. O PBLICO veio dar imprensa diria portuguesa uma nova dimenso e, pelo seu aparecimento, obrigou os grandes (JN e DN) a reformular a sua postura e tambm o seu grafismo. +Tal como na ltima final do Campeonato do Mundo, disputada em Nova Iorque e Lion, Kasparov tenta apresentar-se como o bom reformador contra o mau conservador (Karpov). +A FIDE, que organiza a competio desde 1948, declarou estar pronta para defender os seus direitos em tribunal, e acusou Kasparov e Short de privilegiarem os seus interesses econmicos ao tentarem conseguir uma verba superior oferecida pela cidade de Manchester para organizar a final e que rondava os 250 mil contos. +Este golpe de Estado deixa cptica a maior parte grandes mestres de xadrez (cerca de 300 em todo o mundo), que esperam ver a situao clarificada, independentemente da parte que acabe vencedora. +Por outro lado, a expulso de Kasparov, detentor do ttulo mundial desde 1985, criar dificuldades FIDE para convencer Manchester a acolher uma falsa final entre Karpov e Timman. +Isto no caso de os dois xadrezistas aceitarem o convite. +A bem dizer, todos tm razo. +O rbitro porque o terreno de jogo estava quase impraticvel. +Henrique Calisto porque o terreno estava quase impraticvel, o Leixes defendera o adiamento da partida antes do seu incio e tinha menos um homem em campo que o adversrio, por expulso de Srgio. +Eurico e Milton Areal porque o terreno estava quase impraticvel, mas nem melhor nem pior do que na primeira metade do encontro e o Tirsense parecia mais fresco e estava em situao de vantagem numrica. +Quanto ao jogo, ele dividiu-se em dois perodos distintos: antes e depois da expulso de Srgio. +Antes, o Leixes marcou um golo, por intermdio de Maurcio, na sequncia de um canto apontado por Barriga, e foi a equipa que melhor se adaptou s condies do terreno. +Depois, o Tirsense tomou conta da partida, criou vrias situaes de golo e conseguiu a igualdade atravs de uma grande penalidade marcada por Rui Manuel, a castigar carga de Correia sobre Batista. +Com o dilvio como pano de fundo, o empate traduz de forma feliz um jogo que ficou no meio. +Durante a anlise do relatrio de actividades passadas, foram identificadas como principais insuficincias a ausncia de uma orientao nacional junto dos quadros tcnicos, e o fraco recrutamento e pouca contribuio na rea da Cincia e Tecnologia. +O Partido Popular (PP), vencedor das eleies de 3 de Maro, quer a plena integrao da Espanha na Aliana Atlntica, organizao a que Madrid aderiu em 1982, sem, no entanto, englobar as suas foras militares nas da NATO, de acordo com os resultados do referendo de 1986. +Um processo que ocupar os prximos ministros da Defesa e das Relaes Exteriores e que no ter a oposio dos socialistas de Felipe Gonzlez, a segunda maior fora poltica do pas. +O russo ser uma das seis lnguas principais usadas por Joo Paulo II amanh e depois, no Encontro Mundial da Juventude, no Santurio Mariano de Czestokowa, onde se prev a presena de um milho de jovens, o dobro dos que h dois anos se congregaram em Santiago de Compostela. + que, alm de a proximidade geogrfica da URSS e dos outros pases do Leste, muita coisa aconteceu nos ltimos 24 meses na grande Casa Comum Europeia. +Ao chegar, s 9 horas (TMG) de hoje, ao aeroporto de Cracvia, Joo Paulo II ser recebido com um mnimo de formalidades. + a quinta vez que Karol Wojtyla pisa, como Pontfice, o solo da sua ptria. +Mas convencionou-se que esta sua deslocao de trs dias, para presidir ao Encontro dos Jovens em Czestochawa, seria considerado um complemento ainda da sua recente viagem Polnia, de 1 a 9 de Junho passado. +Carrington fez sempre questo de salientar que as hipteses de sucesso do cessar-fogo dependem sobretudo dos beligerantes. +Se for firmado, ningum ficar mais contente do que ns. +Eu tentei, o senhor Vance tentou, se for respeitado, urrah!, comentou. +Mas se falhar? +Ningum fez mais at agora do que o secretrio Vance. +Devemos tentar. +A situao pode ser potencialmente horrvel. +Pela primeira vez no Haiti um padre foi assassinado por motivos polticos. +Uma mensagem dos militares no poder para mostrarem quem ainda manda no pas, interpretam meios eclesisticos, que reafirmam a disposio de continuar a luta pela libertao do povo haitiano. +A Conferncia Haitiana de Religiosos, cuja direco tida por moderada, vem respondendo ao crime com jejum, oraes, missas ao ar livre e homilias em que o engajamento do padre Jean-Marie ao lado dos pobres e oprimidos apontado como exemplo. +Pode vir a ser o ponto de viragem na Igreja, cuja hierarquia, com a excepo de um nico bispo, prefere os militares golpistas a Aristide, o Presidente eleito democraticamente, ele prprio um padre que os salesianos expulsaram da ordem. +H, no ar, uma certa ideia de invaso. +Cavaco Silva no assinou o habitual despacho que d tolerncia de ponto na tera-feira de Carnaval- o que significa que, ao contrrio do que tradicional, este ano no h o feriado do Entrudo. +Ontem, comeou a chegar s direces-gerais (atravs de uma circular) a informao de que o dia 23 ser um dia normal para os funcionrios pblicos. +Registe-se que a tera-feira de Carnaval no um feriado legal, mas to-s tradicional: ou seja, todos os anos o primeiro-ministro tem que produzir um despacho, publicado em Dirio da Repblica, em que decreta a tolerncia de ponto. +Este ano, eventualmente condicionado pela polmica que o ope ao Presidente da Repblica em torno dos feriados (recorde-se que Soares enviou a lei para o Tribunal Constitucional) Cavaco decidiu, pura e simplesmente, acabar com a tera-feira de Carnaval. +Alis, era contra as teras-feiras, propiciadoras de sugestivas pontes, que a lei governamental mais se batia ... +Desde 1990 que estava na mesa a reformulao das secretas. +A primeira inteno do Governo era ver legislado algo que, ignorando a lei, era um facto desde o incio: a inexistncia efectiva do Servio de Informaes Estratgicas de Defesa (SIED), que nunca foi mais que uma alnea da lei-quadro nunca levada prtica. +Com argumentos economicistas e de operacionalidade, o Executivo de Cavaco Silva sempre se escusou a concretizar o SIED, cujas competncias foram, entretanto, transferidas para o SIM (Servios de Informaes Militares), por via de um polmico acto administrativo do Governo, que assim chamava a si matrias da exclusiva competncia da AR. +Ou seja, em dez anos, nunca a Lei dos Servios de Informaes foi integralmente cumprida, com uma estrutura que estava no papel sem existncia prtica (o SIED) e outra que assegurava as funes da primeira (a Dinfo). +Facto que, ao longo do tempo, foi repetidamente denunciado, tanto pelos partidos da oposio (onde se destacaram o PCP e o PS), como pelo Conselho de Fiscalizao dos Servios de Informaes nomeado pela AR. +O caso ocorreu numa noite de 1978, na ilha de Carvalo, ao largo da Crsega. +O prncipe jantava com amigos num restaurante deste paraso para milionrios, quando um grupo barulhento de jovens da alta sociedade italiana acostou na enseada de Palma, ao lado do seu iate, o L'Aniram. +Os advogados da defesa sublinharam no processo que este facto perturbou altamente o senhor de Sabia. +Naquele ano, as Brigadas Vermelhas (BR) estavam no auge da actividade terrorista, o lder cristo-democrata Aldo Moro acabara de ser raptado, e o prncipe -- proibido de entrar em Itlia desde o exlio do pai em 1946 -- teria mesmo recebido ameaas das BR. +O certo que, pouco depois, Vtor-Emanuel apercebeu-se que um barco pneumtico fora deslocado do seu iate e atracado ao Cocke, o navio dos jovens italianos. +Irritado com este acto de apropriao, foi buscar uma espingarda US 30 semiautomtica, utilizada em safaris, e 31 cartuchos, e dirigiu-se para o Cocke. +Pouco depois, o prncipe aponta-lhe a arma ao ventre. +Na confuso que se segue, parte um primeiro tiro, depois um segundo, e os dois homens caem ao mar. +A bordo do veleiro polaco Dar Mlodziezy, patrocinado pela Cutty Sark, os tripulantes e passageiros tiveram que esperar at s 13 horas de ontem para, finalmente, pisarem o cais. +Ancorado fora do porto de Cdis durante toda a noite, o grande veleiro foi tomado de assalto pelos 18 passageiros portugueses a cantar fados at de madrugada. +Desde as 10 horas da manh, as manobras de entrada do porto eram aguardadas ansiosamente e por trs horas toda a guarnio permaneceu no convs, pronta para receber ordens do piloto a bordo do rebocador que coordenou as manobras de atracao ao lado do veleiro Esmeralda, da armada chilena. +Os organizadores da regata ainda davam os ltimos retoques nas instalaes volta do porto quando a frota desembarcou. +Vrias tendas ofereciam os mais variados servios aos tripulantes, como restaurantes, cabines telefnicas, servios bancrios e de correios enquanto na sala de imprensa os empregados da Telefnica ainda instalavam os telefones e mquinas de fax para o batalho de jornalistas que chegou cidade. +Mesmo com a confuso administrativa da escala, o show nutico continua a todo o pano por quatro dias. +A Netscape Communicationns decidiu adquirir a Collabra Software por 108,7 milhes de contos (16,3 milhes de contos). +A Collabra edita o groupware e o software que permitem compatibilizar diversas redes de computadores, nomeadamente as que existem no interior de uma mesma empresa. +Os termos concretos da transaco no foram tornados pblicos mas os analistas coincidem na interpretao deste negcio como mais um passo da Netscape para transformar a Internet num meio privilegiado de comunicao e informao escala mundial. +A Netscape o mais importante fabricante de software de navegao para a Internet. +A LG Electronics, o terceiro maior fabricante sul-coreano de informtica, decidiu reduzir os preos dos seus computadores pessoais. +A deciso visa tornar mais competitivos os produtos da empresa, que est a sofrer uma forte concorrncia dos seus principais concorrentes. +As descidas oscilam, consoante os produtos, entre 9,8 e 26,9 por cento. +Durante o primeiro semestre do corrente ano, o mercado de computadores pessoais da Coreia do Sul registou um aumento de 42 por cento relativamente a idntico perodo do ano transacto, num montante de 1300 milhes de dlares (195 milhes de contos). +O anncio divulgado na sexta-feira pela OMS dizia que uma srie de testes iniciais, realizados pelo Instituto Pasteur de Paris, em amostras de sangue das primeiras nove pessoas a morrerem eram consistentes com o diagnstico de febre de Ebola. +Esse mesmo diagnstico j tinha sido feito a ttulo provisrio apenas com base nos sintomas dos doentes. +Uma equipa daquela organizao encontra-se desde sexta-feira passada na regio para examinar as vtimas e colher amostras. +O Governo gabons, num comunicado em que d conta da existncia de uma epidemia na regio, pede aos habitantes que no evacuem os doentes nem para a capital da provncia, Makokou, nem para a capital nacional, Libreville, e que alertem para qualquer novo caso as autoridades sanitrias, para que estas possam providenciar o tratamento dos doentes in loco. +Recomenda-se ainda s pessoas que no toquem com as mos nuas nem nos doentes nem nos mortos e que evitem o contacto com o seu sangue, vmitos e excrementos. +Sempre que surge um problema, chamam-na. +L vai Dolores Fasca ver o que se passa porta do Nacofino. +s vezes acontece, mas normalmente nunca h problemas graves. +C dentro ningum se apercebe de que um teimoso brio quer impor a sua presena. +O homem insiste, meio zonzo, a patroa no cede e ele acaba por ir-se em grande dificuldade. +Tambm mal atina com as manobras precisas para sair do parque de estacionamento privativo do dancing. +Acaba por se lanar estrada dos Quatro Caminhos rumo a Quarteira, sem temer o balo que agora transforma sopros em cadeia. +Faz frio. +So 2h25, j s resta um casal mesa. +s 0h00 eram quatro. +As relaes de Hong Kong com a China esto a condicionar a evoluo do mercado accionista local. +So as trocas comerciais, o novo aeroporto internacional, entre outros aspectos. +Os investidores, cada vez mais sensveis, esto a reagir prontamente no tomando posies. +Assim, est a registar-se um abrandamento na procura com a consequente queda das cotaes. +O ndice Hang Seng caiu 2,47 por cento, fechando nos 5481,61 pontos. +A Comisso Europeia considerou ontem politicamente inoportuno avanar com uma proposta de harmonizao dos impostos sobre os produtos energticos que, a concretizar-se, poderia provocar um aumento do gasleo em Portugal de quase 25 escudos em 2002. +Os moradores so convidados a fazer desde logo a separao dos lixos -- condio necessria para o xito do projecto --, que depois sero recolhidos por viaturas equipadas a preceito. +Neste sistema, a recolha far-se- uma vez por semana para os materiais orgnicos e trs por semana para os restantes resduos domsticos. +Em Gondomar, a experincia comear com sacos de plstico em vez de cestos, por vontade da prpria autarquia. +Ao mesmo tempo, ir manter-se a recolha indiferenciada tal como hoje a conhecemos. +Tendencialmente, o caminho ser para aprofundar a recolha selectiva, acentuou o mesmo responsvel da Lipor. +Este sistema de recolha adequa-se a edifcios baixos, com poucos pisos. +Em os prdios com muitos andares, haver um ecoponto para todos os moradores. +O nico trabalho das pessoas separar o lixo e colocar nos dias certos os contentores e cestos para serem recolhidos. +O resto connosco, garante a Lipor. +Na Maia, cada morador receber esses recipientes pessoalmente, da mo de funcionrios municipais, enquanto alunos dos cursos de Relaes Pblicas e Psicologia do Instituto Superior da Maia explicaro como funciona o sistema. +Esta aco poder comear j em Julho, com abordagens porta a porta. +Para muitos analistas o verdadeiro problema o facto de no se poder falar do desenvolvimento da economia palestiniana como um facto isolado, porque ele s faz sentido integrado no desenvolvimento de todo o Mdio Oriente. +que tipo de relao econmica se poder estabelecer entre Israel e os seus vizinhos rabes? +Ser prematuro falar numa comunidade econmica entre a Jordnia, os territrios palestinianos autnomos e Israel? +outros defendem precisamente o contrrio. +Fundamental para os palestinianos a abertura de novos mercados, tanto a Ocidente como a Oriente -- e em Israel -- para os produtos que, apesar dos bvios problemas da sua agricultura e da indstria, venham a produzir. +Outra das apostas o turismo, esperando-se dois milhes de visitantes por ano e a criao de 30 mil a 50 mil postos de emprego. +Quanto s anunciadas sadas de alguns jogadores, Donner afirmou que essas notcias no foram feitas por jornalistas, mas por pataratas. +Dizia-se que o Carlos Resende e o Filipe Cruz podiam ir para a Alemanha e o Carlos Galambas e o lvaro Martins para o Benfica, mas mentira. +J todos renovaram os seus contratos. +Acho que o FC Porto s tem de se preocupar com o Sporting, no pensamos em mais nada. +A fartura de pensamento pode dar maus resultados e ns no queremos ter um enfarte. +Esta uma prova de regularidade e s pode beneficiar quem for mais regular. +R. -- No sou capaz. +Sou formada em Direito, mas no conseguiria ensinar algum a ler e a escrever. +Nunca dei uma lio na vida, mas tenho pena, porque penso que se aprende muito a ensinar. +P. -- Como a sua relao com o piano? +No sei. +Talvez morta. +Para estes haitianos, s h uma maneira de regressar a casa: revoluo. +Voltando rua que durante dcadas evocou o talentoso marido de D. Maria II, citemos mais um passo do j referido relatrio da Cmara do Porto, que nos permite avaliar at que ponto a concretizao da Rua de D. Fernando ficou aqum das intenes que lhe estiveram na origem. +A convenincia desta rua palpvel; uma cmoda estrada desobstruda de tortuosidades e declives, desde a Foz ao corao da cidade, especialmente para seges e carros, o que at aqui mal se consegue antes de chegar ao stio do banco de S. Domingos [ leia-se o Banco Comercial do Porto, que fora fundado poucos anos antes e detinha autorizao para emitir notas ]. +Comea a dar resultados a poltica da Unio Europeia de bloquear todas as tentativas suas de gozar as vantagens da UE, sem as responsabilidades de um pas membro. +Os seus governantes j perceberam que o isolamento j no d lucro e pode levar rejeio. +Quando o povo suo recusou, em 92, a adeso ao Espao Econmico Europeu, como j fizera com a ONU, cometeu um grave engano. +Foi essa voluntria e pretensiosa rejeio dos vizinhos, que deixou a Sua sem a cobertura europeia, na crise que destruiu a sua imagem. +O fim da guerra fria, com a imploso da URSS acabou com a importncia helvtica no tabuleiro europeu, enquanto os recentes escndalos de cumplicidade com os nazis, mais o roubo das economias dos judeus pelos seus bancos lhe tiraram a simpatia americana. +No entanto, alguns dos analistas contactados pelo PBLICO, consideram que a Sumolis tem sido esquecida pelo mercado e que existem boas perspectivas quanto aos resultados de 1997. +Para alm destes dois aspectos, surgem os habituais rumores sobre um eventual interesse comprador por parte de outros grupos empresariais do sector das bebidas, como, por exemplo, a Jernimo Martins. +Uma possibilidade entretanto desmentida pelo grupo presidido por Soares dos Santos. + totalmente falso que a Jernimo Martins esteja interessada na compra da Sumolis, garantiu ao PBLICO um porta-voz da empresa. +No lado das subidas, destaca-se ainda o comportamento do Banco Totta & Aores que, ao contrrio dos restantes ttulos do sector bancrio, encerrou a ganhar 3,41 por cento. +Movimentaram-se cerca de 285 mil ttulos, com a cotao de fecho a situar-se nos 4359 escudos. +Alguma coisa se passa volta deste papel. +Existe um forte interesse dos internacionais, salientou outro responsvel. +Inez Teixeira uma jovem pintora que tem exposto regularmente desde h uns dois anos. +Agora, num espao de exposies tambm recente, mostra uma srie de obras de pequenssimo formato feitas a grafite sobre tela. +E julgamos ainda estar longe de casos como o do banco britnico NatWest que guardava as opinies religiosas e polticas e mesmo os hbitos alimentares de alguns dos seus 6,5 milhes de titulares de contas. +Em paralelo, h sempre o perigo de estas BD irem cair nas mos de pessoas menos escrupulosas. +Em Janeiro de 1994, noticiava-se a introduo na Alemanha de um supercomputador que regista as impresses digitais dos candidatos a asilo poltico, para tentar detectar fraudes nos subsdios da segurana social. +Na mesma altura, comeou a funcionar um sistema informtico que permite distribuir mais rapidamente os estrangeiros pelos campos de refugiados. +Nada fez parar esta compilao de dados, nem o receio de que os endereos pudessem cair nas mos de grupos nazis que ficariam assim a conhecer onde moram os seus alvos. +Agora, a polcia inclina-se para que o assassinato tenha a ver com a promoo de John Gotti Junior ao cargo de chefe da famlia durante o encarceramento do pai. +Segundo fontes policiais citadas pelo New York Times, o atentado poderia ter partido da velha guarda do cl Gambino. +Desde que Gotti foi preso, em Dezembro passado, o seu filho, de 26 anos, est a substitu-lo, sobretudo na colecta de fundos resultantes de diversas actividades ilegais. +De acordo com o testemunho de informadores, Junior estaria a manter uma arrogncia excessiva em relao aos velhos membros do cl, indo ao ponto de reclamar somas superiores ao que era habitual. +O motorista acompanhava frequentes vezes John Junior e a sua morte pode ser considerada como um aviso da velha guarda. +Dezenas de timorenses e portugueses ocupam pacificamente o pavilho indonsio da Expo-92, em Sevilha. +Bush s poder ganhar as eleies presidenciais se fizer cair Saddam Hussein, vaticinam os analistas polticos. +O Presidente cancela todos os compromissos e fecha-se na Casa Branca. +Na mesma ocasio iniciaram-se investigaes que incidiram sobre o rbitro madeirense Marques da Silva, tambm ele suspeito de se ter deixado corromper. +A Judiciria aproveitou ainda o balano para passar buscas s casas de Reinaldo Teles (dirigente), Jorge Gomes (funcionrio) e Antnio Garrido (colaborador), todos ligados ao FC Porto, com a curiosidade de o ltimo ser um ex-rbitro de futebol. +se Gumaro era corrupto, quem eram os corruptores e porque motivo no foram igualmente presentes ao juiz do Tribunal de Instruo Criminal? +Essa uma pergunta que ainda hoje permanece sem resposta. + que, apesar de todas as tentativas feitas pelos agentes da Direco Central de Investigao de Corrupo, Fraudes e Infraces Econmico-Financeiras, o rbitro, sujeito a diversos interrogatrios, nunca fez qualquer revelao que pudesse incriminar outras pessoas. +Por outras palavras: nunca quis beneficiar do estatuto de arrependido. +Alice no sabia o que era um fato-macaco, mas no teve coragem de perguntar. +No podamos. +Afinal, os direitos dos trabalhadores esto garantidos na Constituio. +Os temas escolhidos so cinco: Patrimnio Virtual (explorao dos monumentos portugueses usando as tecnologias da realidade virtual); Portugal Global (Sagres como antena de expanso e comunicao da expanso global e local dos portugueses); Sacra Saturni (Sagres como lugar de mistrio e simbologia, tema ideal para um jogo de aventura grfica); Um Milho de Navegadores (Sagres como centro privilegiado de turismo cultural, com um milho de visitantes por ano) e Terrvista (Sagres como ponto de partida para uma abordagem pedaggica, pondo os alunos no papel de produtores de informao). +Numa primeira fase o trabalho ser concretizado num ' site ' na Internet, com acesso universal e que ter sede em Sagres. +Essas pginas na Internet tero uma componente de referncia a Sagres e regio costeira do Algarve, num regime de divulgao e promoo do patrimnio, com um registo diferente do clssico. +Depois, poder haver exploraes em etapas sucessivas para outras linguagens. +Poder-se- tocar na realidade virtual, embora seja um objectivo a longo prazo. +As primeiras semanas foram dedicadas ao estudo dos textos propostos pelos EUA. +Nos ltimos dias o ritmo tornou-se frentico, manipularam-se mais de uma centena de documentos e mapas, garante de la Pea. +Segundo a Newsweek, as duas salas dos mapas foram o palco principal das negociaes. +Estavam repletas de lixo, copos de plstico sujos de caf. +Os negociadores usaram canetas de ponta de feltro para traar linhas de fronteira nos cartes plastificados com o territrio da ex-Jugoslvia impresso. +Bastava um pano hmido para fazer desaparecer as linhas, e recomear tudo de novo. +rbitros: Miguel Castro (Argentina) e Alfonso Bove (Itlia). +Portugal -- Guilherme Silva, Paulo Almeida, Vtor Fortunato, Pedro Alves, T Neves; Rui Lopes (2), Paulo Alves e Antnio Ramalho (2). +H muito tempo que tenho uma estranha relao afectuosa com esta ilha. +No quiosque vendem-se dessas revistas de viagens que agora proliferam e que perpetuam as fantasias sobre ilhas exticas. +Sempre me pareceu estranho nunca ter lido um artigo sobre a ilha de Santos. +Estava convencido que s eu a via, s eu a imaginava vista de cima naufragando no meio dos horrveis autocarros lisboetas. +Protegida pelas correntes anti-estacionamento selvagem, todos os dias suspirava de alvio por ainda ver a ilha no seu stio, com as fronteiras bem definidas -- o que em si uma das razes que faz das ilhas um dos nossos arqutipos mais resistentes. +Outro dia reparei numa bandeira hasteada no passeio em frente. +Era a bandeira duma organizao que eu desconhecia: Amigos da Ilha de Santos. +Como algum que descobre no estar s no mundo, aquela bandeira foi alimento espiritual. +Eis que a minha ilha tinha bandeira e tudo. +Eis que, afinal, existem mais habitantes virtuais daquele pas que me d gosto imaginar como um principado independente. +Sobretudo foi bom descobrir que h mais gente a fazer parte do equipamento imaginrio da ilha. +Isolada dos passeios que bordejam os quarteires, frequentada por uma populao mvel, provisria e em constante renovao, a ilha serve de plataforma de comunicaes: apanhar um transporte, comprar um jornal com notcias, enviar uma carta, fazer um telefonema. +Os seus habitantes so do mais cosmopolita que h. +A cimeira sindical ibrica j no dever realizar-se este ano, segundo apurou o PBLICO. +Um reunio ao mais alto nvel a realizar na segunda-feira entre a CGTP e a UGT (na sede desta ltima central) poder desbloquear anteriores dificuldades e levar marcao de uma data. +Na Europa Ocidental tem-se assistido a uma queda acentuada da fecundidade, o que fez surgir o problema da no substituio das geraes. +Cada vez nascem menos bebs, com o inevitvel envelhecimento progressivo da populao. +entre 1960 e 1991, os valores da taxa de fecundidade passaram de 94,9 por cento para 47 por cento. +Quer isto dizer que a percentagem da populao nacional que, em determinado perodo, procriou baixou para metade em quatro dcadas. +Os Padres Recentes da Fecundidade em Portugal, estudo que ser lanado na prxima semana, foi elaborado pelas socilogas Ana Nunes de Almeida e Cristina Ferreira e pelas gegrafas Filipa Ferro e Isabel Margarida Andr. +Editado pela Comisso para a Igualdade e para os Direitos da Mulher, este trabalho pretende contextualizar a queda recente e vertiginosa da fecundidade em Portugal. +Com a acareao entre Paradela de Abreu e o cnego Melo, terminaram anteontem as diligncias previstas no acordo que determinou a reabertura do processo do padre Max. +Para a acusao, o balano positivo, mas para a defesa um novo arquivamento do caso est mais prximo. +At porque o juiz acaba de indeferir uma acareao entre os sete suspeitos. +0 juiz titular do processo do padre Max, Artur Oliveira, indeferiu um pedido do procurador-geral adjunto nomeado pela Procuradoria-Geral da Repblica para acompanhar a investigao deste caso, no sentido de promover uma acareao entre os sete indivduos indiciados na acusao provisria como responsveis pelo crime-- trs como autores morais e os restantes como autores materiais. +Paulo S pedia ainda uma acareao entre o industrial portuense Manuel Macedo, Ramiro Moreira e o tenente da Marinha Pedro Menezes, todos testemunhas neste caso. +Todos os nomes citados derivam desta cena, que rapidamente foi superada por outras modas. +Qualquer deles prosseguiu na linha da pop electrnica, todavia, s Marc Almond e os Erasure seguem hoje uma atitude camp, embora ambos com algumas nuances que de algum modo tendem a atenuar-lhes a envolvncia escandalosa. +Almond o nico que continua a cantar teatral e amaneirado, fazendo das suas interpretaes casos de incandescncia incontrolvel, assente no culto do personagem instantneo, boa maneira tradicional do camp de Oscar Wilde. +Se isso fica uma vez mais reiterado em Memoribilia, a compilao dos seus xitos a solo e nos Soft Cell, que parcialmente regravou para o efeito, tambm visvel que nos respectivos novos vdeos colados s antigas canes qualquer coisa mudou em Almond. +O exemplo paradigmtico Say hello and Wave Goodbye, cujo primeiro clip consistia numa verdadeira orgia de excessos e que agora substitudo por um teledisco de um romantismo asseado repleto de modelos em cmara lenta, que mais se tende a ligar aos dessexuados Black ou Don Henley. +Em sntese, aquele dinheiro seria a ltima prestao do pagamento do terreno. +Em troca, assinava-se o contrato-promessa de compra e venda. +Deolinda, com o seu advogado ao lado, deu-lhe a procurao e Manuel foi ter com Constantino para ele lhe dar tambm os papis assinados. +E Constantino perguntou-lhe se ele estava a brincar, pois no lhe ia dar procurao nenhuma enquanto no recebesse a sua parte, ao que Manuel, (se ningum no tribunal mentiu neste aspecto), lhe replicou que j tinha dado os 1095 contos a Deolinda e eles que dividissem a soma entre os dois. +E viu ento pela cara de Constantino que tinha feito mal, mas muito mal, pois marido e mulher agora s partilhavam uma filha, um barraco e um dio horrvel, azedo e mtuo. +De facto, dava a impresso que marido e mulher, nesta ltima fase do casamento, s j comunicavam atravs dos respectivos advogados, um luxo estranho para pessoas quase sem dinheiro. +-- E ela disse para passar um cheque ... +A Assembleia aprovou ainda moes que reclamavam a divulgao dos resultados provisrios da avaliao das universidades, a fiscalizao da constitucionalidade da Lei de Financiamento do Ensino Superior Pblico, e insistiam nas campanhas de divulgao das queixa dos estudantes, aproveitando, por exemplo, a presena da comunicao social no prximo jogo Acadmica-Benfica. +Entre as propostas mais ousadas, decidiu-se pedir ao Presidente da Repblica que proponha um referendo sobre a Lei do Financiamento, desafiar as televises a promoverem um debate entre o ministro da Educao e todos os dirigentes associativos, pintar de negro a sede da Direco Regional de Educao do Centro e remeter envelopes com folhas de papel higinico ao ministrio. +E no seria mais til que o dr. Soares que, enquanto primeiro-ministro, deixou a educao no caos que se conhece, dirigisse mensagens a lanar o debate sobre o que h a reformar no ensino, em vez de passar a vida a exigir uma televiso to livre que Eduardo Moniz tenha liberdade para criticar toda a gente excepto .. o dr. Soares? +... E, se o dr. Soares tivesse praticado desporto na escola, ser que, hoje, pensaria da mesma maneira? +A VW ainda no tomou qualquer deciso, porque esto a analisar as vrias hipteses. +Mira Amaral confirmou, no entanto, que Portugal uma das possibilidades que esto a ser estudadas pelos alemes, tendo-lhe sido colocadas vrias perguntas sobre as condies de investimento, quando da recente visita a uma das fbricas da VW, na Alemanha. +Para o ministro, a deciso dever ser tomada em meados de 1993 e, se a escolha recair sobre Portugal, essa ser uma boa altura em termos de incentivos. +Em 1993 j estaro esgotadas as verbas do actual Quadro Comunitrio de Apoio [ QCA ] e ainda no estaro disponveis as do QCA de 1994. +V.C. -- Mas quase disco de prata [ faltam seis mil, segundo a editora, a BMG ]! +Comprar um CD no est ao alcance de todas as pessoas que compravam os nossos discos, mas acreditamos nos jovens, porque temos a certeza que aderem a isto. + lgico que estamos a aproveitar o facto de os portugueses aderirem outra vez msica popular. +P. -- E qual a vossa opinio sobre a msica portuguesa actual? +O que que acham que mudou desde os anos de apogeu do conjunto? +A RSSIA anunciou ontem ter assinado um contrato para o fornecimento de mais dois reactores nucleares ao Iro, mas negou ter recebido uma encomenda para vender 4000 blindados ao Iraque. +Quanto ao contrato com Teero, cujo montante no foi especificado, o ministro russo da Energia Atmica explicou que ele inclui a venda de dois reactores de tipo VVER-440, com uma potncia de 440 megawatts, para serem instalados em Bouchehr, no sul do Iro. +A Rssia, insensvel aos protestos dos EUA e de Israel, j se tinha comprometido, no incio do ano, a instalar naquele mesmo local um reactor de 1000 megawatts, num negcio avaliado em mil milhes de dlares. +Em relao ao Iraque, Valeri Progrebenkov, porta-voz da sociedade de Estado Rosvooroujenie, responsvel pelas exportaes militares, desmentiu a existncia de uma encomenda de 4000 carros de combate russos, como afirmara o genro de Saddam Hussein que desertou para a Jordnia. +Segundo este, os blindados seriam entregues ao longo de vrios anos e pagos em petrleo, depois do levantamento das sanes impostas ao Iraque. +Os New York Knicks venceram tera-feira no seu reduto os Chicago Bulls, por 96-91, passando a liderar, por 2-0, a final da Conferncia Leste da Liga Norte-Americana de Basquetebol Profissional (NBA), que se disputa melhor de sete encontros. +O Governo inaugurou pontes e estradas, mas no foi capaz de inaugurar uma Lei de Bases do Ordenamento do Territrio. +A denncia foi feita ontem, em Faro, pelo lder do Partido da Terra (PT). +Ribeiro Teles defendeu, no Jardim Alameda, a necessidade de uma poltica que tenha os ps bem assentes na terra, sem estar subordinada aos nmeros da macroeconomia, construda custa da degradao dos nossos recursos. +O grupo de fotgrafos participantes encontra-se para um confronto pela diversidade no s de estilos, como de opes relativamente construo de uma ideia da cidade. +Assim, Nuno Flix da Costa, que traa um percurso cronolgico -- com uma sequncia que comea ao amanhecer e acaba na noite escura lisboeta, das ruas e e dos bares -- integra constantemente o elemento inslito (ou tornado inslito pelo olhar), da existncia humana e urbana, na paisagem quotidiana: a rotina dos gestos e dos comportamentos. +A sina do seu destino, abre a sequncia de imagens do autor, que fecha com a trmula, brilhante e opaca, potica e tensa, vida da noite. +Antnio Pedro Ferreira, fotgrafo que re-afirma a sua originalidade na busca do acontecimento humano. +No h fotografias sem pessoas, ou sem os seus vestgios, o que no deixa de nunca de remeter o trabalho deste fotgrafo para um dos domnios mais fascinantes, ou encantatrios, mgicos da fotografia, o retrato, ou a fotografia como lente humanstica ... +permanece o registo dos negros, das sombras e da aluso. +Para 1995, a administrao da PT calcula que os lucros atinjam valores prximos dos 25 milhes de contos e pretende manter os nveis de investimento, verificados em 1994. +Dentro da poltica de racionalizao de estruturas, a PT pretende ainda baixar para quatro milhes de contos o valor das existncias em armazm (encontra-se em sete milhes, contra os 11 milhes que se atingiam em Maio) e alienar 40 imveis espalhados pelo pas. +Botelho da Costa garantiu tambm que o oramento para 98/99 ser entrega ao conselho fiscal a tempo de ser analisado por este, provavelmente no final desta semana, incio da prxima. +O Sporting de Braga-Guimares est a suscitar grande interesse na regio minhota e a procura de bilhetes tem decorrido em bom ritmo. +Embora o Guimares no tenha organizado as habituais excurses, praticamente garantido que o Estdio 1 de Maio registar uma das melhores assistncias da poca. +Na hora de defrontar o Sporting, o Famalico pode finalmente respirar aliviado com a notcia do regresso dos seus dois jogadores argelinos, Medane e Menad, que se ausentaram para disputar a Taa de Africa. +Os argelinos, que desempenham um papel preponderante na equipa de Josip Skoblar, chegaram anteontem noite a Famalico, quase uma semana depois de a sua equipa ter sido afastada da luta pelo ceptro mximo do futebol africano, tendo j ontem treinado com o resto do plantel famalicense. +Com o regresso de Medane e Menad, o treinador jugoslavo do Famalico tem sua disposio todos os jogadores, com excepo do jovem Manuel Jos, que se encontra lesionado. +O que a verdade, o que a fico? +Isso no podemos saber. +O que um escritor? + um topgrafo, um investigador, um reprter, ou um fotgrafo, como perguntou um dia Herv Guibert a Peter Handke, numa entrevista? +Quando morre algum, e depois ns ficamos sempre a pensar que dessa pessoa no sabamos quase nada, escolhemos uma pequena parte, e, como dessa pessoa j no conseguimos ver nem os olhos nem as mos, ficamos com essa pequena parte para ocuparmos o espao todo que resta no stio da pessoa morta, ficou-nos s essa parte, muito pouco, por isso depois tem de ser aumentada. +Uma parte de Herv Guibert. +Um livro. +O ttulo Des Aveugles, e foi publicado na Minuit em 1985. +Depois de L'Image Fantme, de Les Aventures Singulires, de Les Chiens, de Voyage Avec Deux Enfants, de Les Lubies d'Arthur. +Antes de Mes Parents, e muito antes dos livros-Sida. +Des Aveugles to nitidamente cruel e generoso como o cinismo quando absoluto e coincide com a ingenuidade. +O lugar do livro uma instituio para cegos. +As personagens so cegos, atrozes e desconcertantes como nas fotografias de Sander, porque os cegos so pessoas que se dirigem incessantemente para um lugar interminvel de onde no podem sair e onde ns nos sentimos gratos por no podermos entrar. +Herv Guibert na poca era jornalista do Le Monde, e pediu autorizao ao Instituto Nacional dos Cegos para fazer uma reportagem sobre Os Cegos e a Cultura, h-de ter sido o que de melhor lhe ocorreu como desculpa para passar l uma semana. +Diz que imaginou que o narrador seria uma espcie de perverso que entra no estabelecimento graas a uma cumplicidade que lhe permite manter relaes amorosas ilcitas com as crianas. +O livro depois mudou, e os cegos tornaram-se personagens passionais, e obscenas na sua violncia, esse livro depois de escrito continha tudo, o infinito, a transparncia, o medo, as imagens do escuro, continha uma criana cega que no admitia no ver e inventava, e Josette que torturava ratos, uma mulher casada, um marido e um amante, e depois havia uma vingana. +Herv Guibert gostava dos cegos. +Depois dessa semana, ficou durante um ano como leitor, ia duas horas por semana. +Apaixonou-se por um cego que no era aluno dele. +Tambm se tornou amigo do empregado da loja onde os cegos iam buscar vdeos. +O vdeo mais procurado pelos cegos era Texas Chainsaw Massacre, de Tobe Hooper. +Foi anos mais tarde que Herv Guibert escreveu o livro. + neste contexto que entre a indstria e a Direco-Geral da Qualidade do Ambiente [DGQA] nunca se interromperam os contactos tcnicos necessrios atempada informao sobre a evoluo verificada no cumprimento do contrato-programa e que permitiro a continuao de uma abordagem responsvel da questo para futuro. +Apraz-nos referir a disponibilidade da DGQA para manter com a indstria um dilogo srio e objectivo. +No conhecendo outra forma de abordar um tema to importante como o da efectiva proteco do ambiente de que tambm somos parte. +Mas alm das afinidades culturais, dos quais os japoneses esto cientes, mas que muitos portugueses desconhecem, h outros pontos de aproximao. +O tema da Expo-98, os Oceanos, ser tambm abordado na Expo-2001, em Yamaguchi, cujo lema geral ser ' O Futuro e o sculo XXI ', ou, dito de uma forma mais potica e numa traduo mais livre do japons, Rumo a um futuro onde brilhe a vida. +Se na primeira metada da dcada de 80 o balano dos confrontos entre os dois eternos rivais era francamente equilibrado, na segunda metade da mesma dcada- excepo feita aos fabulosos 7-1 com que o Sporting venceu o Benfica na tarde de 14 de Dezembro de 1986, em Alvalade- os encarnados foram ganhando vantagem neste muito especial campeonato entre as equipas da Luz e de Alvalade. +A maior evidncia para esta recente superioridade encarnada vai para o facto do Benfica, nas suas trs ltimas deslocaes a Alvalade, ter vencido sempre os lees. +P. -- ... e mais: j depois da derrota continuou a tentar nos jornalistas, e na comunicao social, os bodes expiatrios da derrota ... +R. -- ... no generalizo mas houve aspectos na comunicao social que tiveram que ver com os resultados das eleies. +Mas, verdade, estava completamente convencido de que o PSD iria ganhar e, inclusive, poderia ter maioria absoluta. +Enganei-me: no serie nem o primeiro nem o ltimo, espero .. +Observador privilegiado, o francs Michel Platini, ex-jogador, ex-seleccionador nacional e actual coordenador do Comit Organizador do Campeonato do Mundo de futebol de 1998 em Frana, deita um olhar sobre aquilo que foi o Mundial dos Estados Unidos. +Por entre crticas falta de vontade dos polticos franceses, faz algumas comparaes e previses. +No que se refere s regras do jogo, considera que o futebol deve continuar a ser futebol. +Da que seja avesso a profundas alteraes das suas regras. +L'QUIPE -- De que maneira viu o recente Campeonato do Mundo de futebol? +No pondo em causa o direito dos clnicos de exigirem melhores condies remuneratrias e de trabalho, Maria Belm lembrou que, para se pedir mais dinheiro, temos de mostrar que fazemos mais e melhor. +O recado no tinha apenas como destinatrios os mdicos algarvios, mas toda a classe. +A este propsito, argumentou que as dificuldades e os constrangimentos do pas exigem um grande controlo dos gastos. +No seu entender, o seu Ministrio j fez um esforo para que os trabalhadores da Sade beneficiassem de um aumento superior ao resto da funo pblica. +Por conseguinte, um novo aumento salarial, de acordo com a ministra, ter necessariamente de ser acompanhado de um aumento de produtividade. +Nesse sentido, adiantou que o Ministrio est disponvel para negociar a alterao dos regimes de trabalho, em sede dos centros de responsabilidade integrados, tendo j convidado os sindicatos para para iniciar esse processo. +No caso do Hospital de Faro, onde existe um conflito entre os cardiologistas e a administrao, renovou a confiana no rgo de gesto. +Maria de Belm aproveitou esta deslocao ao Algarve para inaugurar o centro de sade de Lagoa, que j estava a funcionar, mas encontrou a maioria dos mdicos em greve. +Portugal encontra-se hoje mergulhado numa crise que parece ter-se instalado predominantemente na Europa, mas afecta toda a humanidade. +No h por isso, para alm da prossecuo das polticas que privilegiam o modelo de sociedade escolhido, solues especficas para o nosso pas. +Dez canes para lembrar a carreira de Cassandra Wilson no catlogo JMT, a casa que lhe abriu o mundo. +Uma das mais prometedoras cantoras dos ltimos anos, Cassandra usa um gro de voz e um jeito de desenhar e se apropriar do verso que tem razes em Carmen McRae. +Nascida no seio da esttica M-Base, de que se tornou a nica porta-voz vocal, medida que foi avanando mar adentro, Cassandra soube libertar-se do lastro que lhe ajudou a voz a crescer mas que ameaava paralis-la. +Despojada da preocupao de fazer novo e diferente a cada passo, o canto virou rvore, ganhou espao, trepou ao cu. +No dia em que Wilson inventar o tempo & o modo de combinar a tradio (de que se tem aproximado progressivamente, como o mostra, de forma exemplar, o lbum Blue Skies) com os novos sons que lhe adubaram a voz (do rap e hip-hop inquietao experimental vivida ao lado de Steve Coleman), uma nova porta se abrir ao jazz vocal. +Parte-se depois para o debate. +Paquete de Oliveira, socilogo, fala do drama social. +E Carlos Narciso junta a sua voz daqueles que se surpreendem com o facto de Domingos Pereira, condenado a 15 anos pela morte da mulher, ter cumprido apenas seis. +Fala na opinio pblica. +Era a tirada mais infeliz da noite. +Com programas destes, que correm o risco de valorizar at exausto alguns dos aspectos mais srdidos da histria de criminosos, arriscamo-nos a ter, em breve, uma opinio pblica a pedir agravamentos sem fim das penas, da represso e mesmo o ressurgimento da pena de morte. +Quanto mais os EUA forem capazes de deixar claro que ftil competir com o poder americano, menos chances haver de que outros alimentem ideias de perturbar a actual ordem mundial. +Um outro poder, aliado mas diferente, como pode vir a ser a Unio Europeia, s lhes pode ser til. +E apelava ao idealismo e ao pioneirismo da Amrica como o antdoto capaz de dar sentido ao seu enorme poder. +Mariano Gago falava na sesso de encerramento de uma conferncia sobre A criao da sociedade de informao euromediterrnea, um projecto que surge na sequncia da conferncia de Barcelona (Novembro de 1995), em que foram lanadas as bases de uma cooperao mais estreita -- a nvel econmico e poltico, mas tambm cultural -- entre os pases das duas margens do Mediterrneo. +As novas tecnologias da informao permitiro construir rapidamente este smbolo moderno da nossa vontade comum de criar, entre as elites estudiosas dos nossos povos, laos de solidariedade, compreenso mtua e trabalhos em comum, declarou o ministro. +Atentados suicidas do grupo Hamas em Israel: 25 mortos e 80 feridos. + a vingana pela morte do seu lder Ayyash. +Jos Eduardo dos Santos e Jonas Savimbi renem-se em Libreville, concordando que um Governo de Unidade e Reconciliao esteja formado at Junho ou Julho. +Antnio Guterres possui uma boa relao com o teatro. +Tem pena de ter pouco tempo para assistir a espectculos, mas est atento. +Nos ltimos meses ainda conseguiu escapar-se e ver Eu, Feuerbach, pelo Cendrev, em vora, e A pera do Malandro, pelo Seiva Troupe, no Porto. +E gostou. +As declaraes prestou-as ontem durante o almoo que ofereceu, em S. Bento, s personalidades do teatro. +No dia em que ficou constitudo o jri de apoio ao teatro no governamental. +Um vizinho do Po de Acar de Cascais j no aguenta ouvir as descargas do supermercado. +Queixas atrs de queixas, foi conseguindo umas vitrias. +Agora, porm, tudo esbarrou num muro que a empresa diz ser a soluo mas que o vizinho no aceita. +Afinal, parece que consumir fura os tmpanos. +A Gr-Bretanha pediu na segunda-feira Comunidade Europeia que fosse banido um acar artificial usado na produo de refrigerantes, revelaram fontes diplomticas comunitrias e industriais citadas pela Reuter. +Os britnicos advogam que a substnca em questo, o ciclamato -- 30 vezes mais doce do que o acar -- no seguro para o ser humano e ameaam vetar uma lei sobre a segurana alimentar, que os ministros do comrcio da CEE esto a discutir, se o produto no for proibido. +A Frana e a Grcia apoiam a Gr-Bretanha. +Este tipo de acar foi banido nos Estados Unidos e na Gr-Bretanha depois de uma srie de testes ter demonstrado que o produto provocava cancro nos ratos. +A Gr-Bretanha -- o nico pas da CEE que proibiu o produto -- quer agora banir os ciclamatos da CEE baseados no argumento de que o produto leva, nos animais, atrofia e degenerao testicular. +Mas a comisso da CEE para a segurana alimentar, baseada em peritos dos Doze pases, aprovou os ciclamatos depois de analisar o consumo mdio em relao aos humanos durante um perodo de vida. +Domingos, peito hirto e joelhos levantados, cerra os dentes e vence Buckner (medalha de bronze) e Dlze. +Domingos no se tem em si de contentamento, no quer acreditar no que se est a passar. +Abraa-se ao irmo, que ficou em 8 lugar. +Ganhei a medalha de prata, diz-lhe ofegante, mas logo se recusa a acreditar na realidade. +S quando Moniz Pereira lhe surge na frente, se compenetra de que era mesmo verdade. +O treinador, de sorriso rasgado, abraa-o. +Lisboa, 18 de Fevereiro de 1992. +So 9h 25m e Moniz Pereira arruma o automvel junto ao estdio do Sporting Clube de Portugal, incrustado entre o Campo Grande e o bairro residencial lisboeta do Lumiar. +Metade do Oramento destina-se a despesas correntes, suscitando criticas da oposio, que as consideram exageradas, nomeadamente as despesas com pessoal (mais de um milho de contos). +O oramento prev obras como a construo do pavilho gimno-desportivo de Pinhal Novo e as piscinas do Pinhal Novo, entre outras. +Para o presidente Carlos de Sousa, trata-se de um oramento tpico de um concelho rural e pobre com poucas receitas. +Segundo elementos recolhidos na nica junta de freguesia do concelho, o Entroncamento tem, aps o recenseamento eleitoral do ano passado, 12 480 eleitores, apenas mais 372 do que no ano anterior. +Os responsveis desta junta, bem como os da autarquia, garantem que este nmero est muito aqum da realidade. +se Maom no vai montanha, a montanha vai a Maom. +Alteraes ao Cdigo Civil que incidem sobre o direito da famlia. +Uma proposta de lei a aprovar por a Assembleia da Repblica que permita a alterao do Cdigo de Procedimento Administrativo. +O CONGRESSO Nacional Africano (ANC) reiterou ontem, em comunicado, como insuficiente a remodelao ministerial realizada pelo Presidente De Klerk, na sequncia do escndalo sobre o financiamento secreto de Pretria ao partido zulu Inkhata, anunciando que vai intensificar a sua campanha para obter a mudana do Governo sul-africano por um Executivo de transio. +UM GRUPO de alegados assaltantes, acusados do roubo de diamantes no valor de um milho de contos da central de escolha de uma operadora portuguesa em Angola, est a ser submetido a interrogatrios que forneam pistas sobre uma presumvel rede de trfico. +Cerca de um milho de contos em diamantes, em gemas e para uso industrial, foi roubado na noite de 14 para 15 de Setembro da estao de escolha da Sociedade Portuguesa de Empreendimentos (SPE), no Ocapa, Lunda Norte. +O que consta que os militares tm bastante cuidado para evitar esse tipo de situaes. +Sabendo antecipadamente o grau hierrquico da personalidade que vai actuar, fazem-se representar em conformidade. +Para um secretrio de Estado, um vice-chefe. +No entanto, no Ministrio da Defesa no h grandes preocupaes quanto funcionalidade da gesto se houver vacatura com a provvel sada de Nogueira. +Se os chefes militares, actualmente, despacham com o ministro de 15 em 15 dias, faro o mesmo com o primeiro-ministro e o secretrio de Estado governa o Ministrio. + tudo uma questo de tempo e ainda falta conhecer os resultados do congresso do PSD e saber a altura exacta em que o Presidente da Repblica tomar deciso relativamente ao Governo. +Entretanto, a Comisso Nacional de Eleies (CNE) notificou ontem a SIC pela no cobertura da campanha eleitoral do candidato Jernimo de Sousa, na sequncia de uma queixa por este apresentada. +A estao de Carnaxide tem um prazo de 48 horas para responder, devendo a CNE na sua prxima reunio tomar uma deciso, que, se for desfavorvel SIC, lhe poder custar uma coima entre os mil e 10 mil escudos. +Entre um e dez contos, exactamente. +A Kapiti forneceu ao Banco de Comrcio e Indstria (BCI) os sistemas Equation, back-office para operaes internacionais, e FS-Dealer, front-office para operaes cambiais. +Mas o primeiro grande atleta de Moniz Pereira veio de Viseu, no interior nortenho, com muito frio no Inverno e muito calor no Vero. +Numa freguesia rural s portas da cidade, Vildemoinhos, nasceu e fez as primeiras corridas aquele que viria a ganhar a maratona de Los Angeles em 1984: Carlos Lopes. +Lopes corria na infncia pelas hortas, competia com amigos quando iam a festas na aldeia vizinha, atravessava velozmente vinhas e castanhais. +E era sempre ele quem ganhava. +Na terra natal foi torneiro-mecnico, teve vida dura, at que um dia deu nas vistas nos crosses nortenhos. +O Sporting contratou-o, trouxe-o para Lisboa. +E Moniz Pereira comeou a trein-lo. +O ministro canadiano das Pescas, Brian Tobin, tinha dito, no domingo passado, estar pronto a tomar todas as medidas necessrias para impedir 49 barcos europeus -- 38 espanhis e 11 portugueses -- de continuarem a pescar nos grandes bancos, ao largo da Terra Nova. +Tobin sublinhou que os pesqueiros europeus foram todos prevenidos, via rdio, de que o Canad proteger os seus stocks de solha e palmeta, mesmo para alm do limite das 200 milhas nuticas. +As informaes disponveis do conta da presena de quatro fragatas, um porta-helicpteros e avies canadianos a vigiar os barcos ibricos. +Em causa neste diferendo esto dois problemas: a inteno canadiana de, pretensamente, preservar os recursos de pesca da zona e, em segundo plano, a inteno de alargar a sua jurisdio a guas internacionais para alm das 200 milhas da zona econmica exclusiva. +Os canadianos dizem querer diminuir fortemente a pesca da palmeta para evitar o seu desaparecimento, como aconteceu com o bacalhau e o ' red fish ', observa Ernest Loignon. +A dupla Jorge Bica / Joaquim Capelo regressou ao comando da 42 Volta Galp a Portugal, prova pontuvel para o Europeu de ralis, aps a disputa da segunda etapa. +Uma vantagem de 1m34s sobre os segundos, Jos Carlos Macedo / Miguel Borges, em Renault Clio 16V, garante ao piloto do Lancia HF Integrale uma forte dose de tranquilidade para conseguir a vitria na prova, o que o colocaria em excelente posio para a conquista do ttulo. +O livro foi inicialmente publicado pela Cadernos Tempo, em Moambique. +Recentemente, uma editora uma italiana descobriu-o. +Maria Teresa Pazzolo traduziu-o, prefaciou-o e ilustrou-o, com fotografias de sua autoria. +A AIE-Guaraldi, da Reppublica di San Marino chancelou este livro que se deixa inquietar com o desaparecimento da Ilha de Moambique. +Em Portugal, esta novela no foi ainda editada. +Com a sua publicao em Itlia, talvez O Barco Encalhado desenterre as amarras e aporte no Tejo. +O que seria importante pois, de alguma forma, este livro combate a viso preconceituosa e lacunar que, ao longo dos tempos, Ilha de Moambique tem sido consagrada. +Os ministros europeus do Trabalho e dos Assuntos Sociais aprovaram ontem no Luxemburgo leis comunitrias instituindo uma licena mnima de trs meses para assistncia famlia e a aplicao de regras do pas de acolhimento para os trabalhadores deslocados no estrangeiro, anunciou o ministro alemo Norbert Blum. +Estes dois projectos tinham sido objecto de um acordo poltico durante a ltima reunio dos Quinze, a 29 de Maro em Bruxelas, mas faltava-lhes a adopo formal. +O fogo consumiu inmeros fardos de bacalhau, dois dos trs tneis de secagem artificial, alguns compressores, duas carrinhas utilizadas no transporte dos trabalhadores da fbrica e um carro de marca Triumph, propriedade do comendador. +Os armazns e uma habitao contguas empresa foram tambm danificados pelo sinistro. +Os prejuizos, disse Gonalves Gomes, esto cobertos por o seguro, mas ascendem a centenas de milhar de contos. +O bacalhau queimado estava j embalado em fardos ou disposto nos tabuleiros de secagem e pertencia a lotes de produo da empresa, sendo o peixe importado da Noruega e Dinamarca. +Apesar da quantidade de bacalhau consumida pelas chamas ter sido elevada, Gonalves Gomes afirmou que no ir acontecer qualquer desestabilizao do abastecimento do mercado. +Quanto Empresa de Pesca de Viana estar garantida a continuidade da laborao, embora a nveis de produo mais reduzidos, j que s poder ser utilizado o nico tnel de secagem artificial poupado no incndio. +A comparao perversa e d para os dois lados. +O deputado social-democrata Carlos Coelho, por exemplo, at j se deu ao trabalho de comparar os dois documentos, apenas para provar que h inmeras citaes literais do diploma no articulado do pacto, que assim se transforma num repositrio de ideias globais, que no acrescenta nada ao que a lei de bases e a Constituio portuguesa preconizam. +A forma como todo o processo tem vindo a ser encaminhado , alis, susceptvel de gerar expectativas perversas, porque na forma como as coisas iro ser levadas prtica que as pessoas se dividem. +Escola Secundria de Paos de Ferreira -- Est aberto pelo prazo de trs dias, a contar da publicao do presente aviso, concurso para preenchimento de um horrio incompleto de 6 (seis) horas na disciplina de Socorrismo (11 ano). +Na Gr-Bretanha so mortas, anualmente, cerca de 70 crianas. +As causas so os maus tratos. +Os culpados so quase sempre os pais. +O nmero de crianas menores de cinco anos, mortas por estranhos, tem sido em mdia de uma por ano, pelo menos desde 1982. +Se o assassnio de crianas por estranhos um tipo de crime raro, estranho que isso acontea pela mo dos prprios pais. +Mais inesperado ainda encontrar uma criana assassina. +O Declogo, de Krystof Kieslowski . +Qual foi, para si, o principal acontecimento mundial da ltima dcada? +Trs jovens portugueses que fazem msica portuguesa. +Assim se definem os Requiem, ex-Requiem pelos Vivos, que aps quase cinco anos de ausncia dos estdios regressam em Outubro com um novo lbum e uma mo-cheia de espectculos. +Pelas rdios, entretanto, roda j Entre o Cu e o Medo, o single promocional. +Duas Viagens, uma exposio de fotografias a preto-e-branco de Francisco Villa-Lobos, feitas em 1995 em Tquio, Nagasaqui e Quioto, nos intervalos de rodagem do filme Os Olhos da sia, de Joo Mrio Grilo, pode ser vista at 7 de Novembro na sala Laman do Centro Cultural de Belm. +Impermanncia -- Um Caminho para o Auto-Conhecimento o ttulo de uma exposio / instalao de Regina Chulam, que joga com vrios auto-retratos pintados pela artista e que pode ser apreciada, a partir das 18h30, na Casa Fernando Pessoa (R . Coelho da Rocha, 16), em Lisboa. +Outros dissidentes conhecidos ainda presos, envolvidos na Primavera de Pequim so Wang Juntao e Chen Zimin, jornalistas, 35 e 40 anos, respectivamente, condenados em 1990 a 13 anos de priso, Liu Gang, estudante, 32 anos, condenado a seis anos, Reng Wanding, operrio, 48 anos, condenado a sete anos, e Bao Tong, 60 anos, antigo brao-direito do secretrio geral Zhao Ziyang, condenado a sete anos. +No havia cruzamento, no se tratou de uma tentativa de ultrapassagem, nem sequer de uma travagem brusca. +Segundo o relato da agncia Lusa, o Volvo deslizou sozinho de uma garagem particular onde estava estacionado. +E s o inesperado encontro com o Mercedes o fez parar. +Sabe-se l at onde poderia ter ido, deslizando pelo piso molhado da Rua das Flores. +O incrdulo automobilista, por no ter interlocutor, achou por bem chamar a polcia e relatar-lhe como tudo se passou. +Afinal, h que apurar responsabilidades em matria de seguros, mesmo em situaes inslitas com esta. +As tropas ficariam fora dessa espcie de estrada, que na altura no foi sequer definida, concentradas em determinados pontos, que tambm no foram definidos. +vo definir esse corredor e determinar os pontos de acantonamento dos soldados. +Se no se incomodarem um ao outro porque h condies para partir para a fase seguinte. +Que tambm no se sabe ainda qual . +Os Fados d'Aqum e d'Alm-Mar que Joo Braga concebeu para o Centro Cultural de Belm traduziram-se numa exibio confrangedora de equvocos e falta de preparao onde couberam amigos, muito Fernando Pessoa, uma boa voz, de Rita Guerra e uma anedota brasileira de mau-gosto. +O fado, esse, ficou aqum. +O defesa portugus Hlder estreou-se no campeonato espanhol e encontrou logo pela frente um seu velho conhecido: o ex-sportinguista Amunike. +Bateu-se bem, ganhou-lhes muitos lances e anulou o flanco esquerdo do ataque dos visitantes, mas acabou por ver o remate decisivo da partida tabelar no seu corpo antes de a bola chegar s redes. +Fui infeliz, a bola bateu-me nas costas e entrou. +Mas, pelo que fez na segunda parte, o Barcelona mereceu ganhar. +Hlder cumpriu bem na sua estreia. +Com a particularidade de ter jogado fora do seu posto habitual. +Penso que foi por o John Toshack saber que eu conheo bem o Amunike. +Pediram-me para desenrascar, mas prefiro jogar a central, explicou o portugus. +O Hlder tem razes para estar contente. +Pelo caminho, tinha repudiado uma insinuao de Damsio, que, no seu discurso, se referiu sua comisso como sendo constituda por crticos ou opositores da actual direco. +E o mesmo fez o orador seguinte, Jos Diogo, este representante do grupo responsvel pela terceira proposta. +A reunio prosseguia ainda hora de fecho desta edio. +Sim, porque as pessoas acabam por passar por todas, apesar das diferenas. +J.S.R. -- Uma das condies fundamentais para se criar outros gostos nas pessoas so os concertos. +C no h apresentao ao vivo de quase nenhuma msica. +E isso muito importante. +Mas nos demais casos em que a lei permite a cobrana coerciva pelos tribunais fiscais, pode ser discutida a legalidade da dvida, o que quer dizer que no h ttulo executivo. +A no ser que estejamos perante um dos documentos do art.46 do Cdigo do Processo Civil, aqui sim, plenamente aplicvel, ou haja lei especial que tal disponha. +A norte de Porto Dinheiro, na Lourinh, elementos da Guarda Fiscal apreenderam, cerca das 4h30 de ontem, uma traineira (presumivelmente registada no porto de Peniche) que transportava 138 caixas de tabaco americano, com valor da ordem dos 22.500 contos. +Na sequncia da operao foram detidos quatro indivduos, entre os quais o mestre da embarcao, a qual foi, tambm, apreendida. +A GF confiscou ainda uma viatura ligeira de marca Bedford, envolvida na rede de contrabando, ontem descoberta. +Quantidade idntica de tabaco -- seis caixas de marca Camel e 124 de Winston -- foi aprendida, na madrugada de tera-feira, na zona de Vila do Bispo, por elementos do comando da Guarda Fiscal do Algarve. +A mercadoria, proveniente de um desembarque, foi encontrada numa carrinha suspeita. +Alverca -- Sem meia dzia de jogadores (os emprestados pelo Benfica), o recm-promovido Alverca ganhou o seu primeiro ponto da poca e logo fora de casa. +A perder, o treinador Mrio Wilson arriscou com a entrada de trs jogadores e saiu de Campo Maior com mais moral para enfrentar a difcil tarefa da manuteno. +Por este andar, Portugal vai ter o campeonato com o ciclo mais alargado de jogos em cada semana, com jogos de sexta a segunda-feira. +Em Itlia os treinadores recusaram os jogos segunda-feira, que as televises queriam impr, em Espanha tambm haver jogos s de sexta a domingo, e em lado nenhum se foi to longe como em Portugal dividindo as jornadas por quatro dias. +H razes tcnicas e tambm de marketing, porque o pblico nunca sabe a que dia joga a sua equipa e a percepo do que se vai passando, para o pblico em geral, tambm no a mesma. +A televiso pode ter assim tanto poder? +No que diz respeito s 200 toneladas de farinha de carne e ossos, a respectiva incinerao fica para mais tarde, em data a anunciar oportunamente pelo Ministrio da Agricultura. +Segundo este organismo, a operao s ter lugar quando for disponibilizado o equipamento que est a ser propositadamente construdo para proceder introduo escalonada nos fornos da farinha obtida a partir da transformao das carcaas dos animais abatidos. +Tratando-se de matrias muito inflamveis -- no possvel retirar integralmente a gordura no processo de transformao --, h que evitar o risco de combusto no momento de entrada no forno (a laborar a alta temperatura), que poderia atingir o prprio operador. +Por isso, as embalagens sero incineradas de forma diferente do habitual, recorrendo-se a um dispositivo que permitir puxar a farinha e lan-la por cima, em completa segurana, para dentro do forno. +Os palestinianos, que querem reivindicar a independncia da Cisjordnia e de Gaza numa fase posterior das conversaes, insistem em ser reconhecidos como parceiros de plenos direitos, mas os israelitas, interessados apenas numa frgil autonomia palestiniana, querem manter-los na delegao conjunta com a Jordnia. +Fontes em Washington disseram France Presse que, para desbloquear o impasse, Israel vai propor a realizao simultnea de conversaes com uma delegao jordano-palestiniana e com uma delegao s de palestinianos. +Pedro Almodovar j filma Kika, onde, com a ajuda do costureiro francs Jean-Paul Gaultier, criou uma espcie de cmara humana. +Nesse filme, uma criada lsbica tem o rosto de Rossy de Palma, o perfil cubista que irrompeu em A Lei do Desejo. +A actriz esteve no Festival Internacional de Teatro de Almada. +Chica Almodovar o que ? +Uma inveno. +Pertence quele grupo de rostos femininos a que chamam as chicas Almodovar, todos eles inventados pelo cineasta de mulheres, Pedro Almodovar. +No caso de Rossy de Palma, a primeira coisa que mostrou no cinema foi mesmo o perfil, ameaador e desregrado como uma pintura cubista. +Foi n' A Lei do Desejo, e depois aconteceu Mulheres Beira de um Ataque de Nervos -- dormiu durante o filme todo, aps um jarro de sumo de tomate e soporferos --, Ata-me! e agora, em rodagem em Madrid, Kika. +O problema do desemprego dos engenheiros e tcnicos do Minseredmash, o antigo e gigantesco complexo da URSS dirigido para a construo do escudo nuclear da ptria, actualmente tema de acalorada discusso na Rssia e constitui uma preocupao para os seus dirigentes. +Os custos desta pesada estrutura tornaram-se insuportveis para o pas. +e um estabelecimento muito chique da capital oferecia aos melhores corredores um par de relgios em ouro. +Era o Maxime ... +Quando me convenci que todos os esforos para tomar o aquartelamento se haviam tornado perfeitamente fteis, comecei a retirar os nossos homens em grupos de oito e dez. +(...) As nossas baixas no combate haviam sido insignificantes, 95 por cento dos nossos mortos resultou da desumanidade do Exrcito aps a luta. +Durante uma semana mantivemo-nos nos cumes da cordilheira da Gran Piedra, enquanto o exrcito ocupava as bases. +No podamos descer, e eles no se atreviam a subir. +No foi a fora das armas, mas sim a fome e a sede, que acabaram por vencer a nossa resistncia. +Tive de dividir os homens em grupos mais pequenos. +outros foram escoltados por Monsenhor Prez Serantes [ arcebispo de Santiago de Cuba ] a fim de se renderem. +Entre os episdios mais importantes a transmitir, alm da entrevista com Giacometti, destaque-se aquele em que se fala da arte de Modigliani, mais ou menos a meio da exibio (nem sempre a ordem respeitada pela RTP). +Fala-se da sua vida: do alcoolismo, do desespero; tambm do charme, da sua gentileza. +E do drama da morte. +Outro episdio importante, Um dia na vida de Man Ray, entrevista filmada no comeo de 1961. +Ou uma memria do surrealismo dita na primeira pessoa. +Montparnasse Revisited assim. +Est cheia destes tesouros. +Os parlamentos da federao croato-muulmana e dos srvios bsnios vo hoje pronunciar-se sobre o novo plano de paz internacional para a repblica. +Os dirigentes croatas e muulmanos j manifestaram o seu apoio ao novo projecto, mas os lderes srvios da Bsnia continuam a manifestar profundas reservas face ao novo mapa territorial proposto pelas grandes potncias. +O jogador representou na poca passada a Associao Desportiva Ovarense, mas foi dispensado em virtude do seu alegado comportamento antidesportivo, que, na opinio da direco do clube vareiro, ter contribudo para a derrota sofrida perante o Benfica na fase decisiva dos play-off. +Sprewer ir ter como colegas na sua nova equipa os seus compatriotas Terrence Radford- que transita da poca passada- e Ruben Cotton, um veterano de 32 anos, naturalizado portugus, que na poca passada esteve ao servio da Oliveirense. +Alexandre Dias, Caetano, Moutinho, Nunes, Paulo Duarte, Rui Santos, Jos Ruivo e Henrique Silva completam o plantel, que ser dirigido pelo tcnico Orlando Simes. +O oramento para a nova poca ronda os 25 mil contos e o principal objectivo garantir a permanncia do Esgueira no escalo principal. +O Beira Mar, a outra equipa da cidade de Aveiro, procedeu tambm no incio desta semana substituio do norte-americano Mike Smith pelo seu compatriota Deshon Washington. +Segundo responsveis do clube aveirense, Mike Smith apresentou-se em deficientes condies fsicas e sem ritmo competitivo. +Deshon Washington, que joga na posio de extremo-poste, tem 23 anos de idade, mede 2,02 metros, e vai fazer dupla com o seu compatriota Kip Jones. +O tcnico do Beira Mar, Aniceto Carmo, tem ainda ao seu dispor os seguintes atletas: Catarino (ex-Esgueira), Paulo Sousa (ex-Salesianos), Rebelo, Traylor, Moreira, Mourinho, Alex Pires, Renato, Joo Miguel e Pinto. +Tendo como objectivo intrometer-se na luta pelo ttulo, o Beira Mar dispor, para a nova temporada, de um oramento a rondar os 40 mil contos, verba substancialmente inferior da poca passada. +Na sua globalidade, as respostas alimentam, apesar de tudo, um certo optimismo entre os que se interessam pela sorte dos bichos. +Para Jorge Rocha, o inqurito demonstrou que h muitas cmaras do pas que tm uma atitude de grande dignidade pelos animais, apesar de serem letra morta as parcas disposies da legislao que regulamenta esta matria e do imenso atraso no nosso pas em relao a outros pases europeus, onde h inclusivamente seguros e assistncia doena. +De um modo geral, os animais domsticos que vivem no campo tm uma existncia mais feliz do que os citadinos. +Nas grandes cidades, os animais domsticos so normalmente mais maltratados, pois, em certa medida, entram em concorrncia com as pessoas, ocupando as ruas, fazendo rudo nos prdios, observa o veterinrio municipal da Cmara do Porto, Vtor Aires. +Nos concelhos rurais, essa concorrncia no existe e os bichos mantm, at, determinadas funes teis, como a guarda ou a caa. +Atrs de Kerrigan na lista das grandes favoritas est a japonesa Yuka Stao, quarta nos Mundiais e que nos ltimos tempos bateu patinadoras de grande nome como a alem Tanja Szewczenko e a canadiana Jose Chouinard. +A China aposta tudo em Chen Lu, terceira classificada nos ltimos dois Mundiais, apontada com a atleta que melhor alia a tcnica e a beleza. +S depois surge Tonya Harding, campe dos Estados Unidos. +E a grande dvida se as suspeitas da agresso sua rival sero um handicap ou uma motivao. +Homens ensopados da cabea aos ps por uma chuva de petrleo, com as chamas e o fumo deixados pelas tropas iraquianas como cenrio de fundo. +As imagens que Sebastio Salgado registou da guerra que ops os Estados Unidos ao Iraque e que comeou pela invaso do Kuwait no mostram msseis Scud, nem a ferocidade dos exrcitos dos dois inimigos. +Revelam a misso perigosa dos homens das 27 equipas de bombeiros enviadas para o inferno que os iraquianos atearam em 788 poos de petrleo quando se retiraram do Kuwait e que permitiram ao fotgrafo brasileiro concluir o seu portfolio sobre o homem e o trabalho. +A fotografia foi publicada na imprensa de todo o mundo h cerca de um ano, quando de o massacre de Bentalha, na Arglia, onde morreram cerca de duas centenas de pessoas, na noite de 22 para 23 de Setembro. +Cinco meses mais tarde, a imagem de uma mulher chorando a perda dos seus oito filhos, baptizada como piet argelina, voltava a ser impressa, desta feita porque o seu autor, escondido atrs do pseudnimo Hocine por razes de segurana, recebia o Prmio World Press Photo 1997. +Tem um projecto pessoal: encenar e interpretar, com a actriz Margarida Tavares, O Amante, de Harold Pinter, em regime de co-produo no Teatro da Trindade, com estreia prevista para Julho. +Tive um professor, Joo Brites, que nos dizia sempre que tnhamos que ter os nossos prprios projectos. +Achei que era importante assumir uma postura pessoal. +Ida e volta do domiclio ao trabalho. +Parti de Cabo Verde h oito anos. +Estava sem trabalho, fazia uns biscates de vez em quando, mas nada de srio. +Se parti, foi porque para mim, como para todos os outros cabo-verdianos, s havia uma escolha: ficar e morrer fome ou partir, no importa para onde, para onde pudesse encontrar trabalho e boas condies para sustentar a minha famlia. +Futebol -- Transmisso directa da cerimnia de abertura do Mundial de Juniores, que se realiza no Estdio das Antas, no Porto. +Futebol -- Transmisso em directo do jogo inaugural do Campeonato do Mundo de Juniores, Portugal-Repblica da Irlanda, a contar para o Grupo A, a partir de as 21h00, no Estdio das Antas. +Porque a imprensa est feita com a direita, como responderia o dr. Cunhal? +Explicao demasiado fcil e desculpabilizadora. +Se assim , porque morreu a imprensa comunista, como o Dirio, porque se afastaram do Partido todos os jornalistas comunistas que trabalhavam em outros rgos de informao? +A resposta outra. +A resposta que o PP, com razo ou sem ela, demagogicamente ou no, para o caso tanto faz, tem tido a capacidade de impor os debates que suscitam a separao de guas, as fracturas polticas de que nascem as opes do eleitorado. +Enquanto que o PCP um deserto de ideias, de discusso e at de coragem ideolgica. +Nada mais previsvel, nada mais desesperadamente igual e repetitivo do que o discurso do PCP. +Existe apenas como uma espcie de museu de ideias. +O PP s tem sete por cento dos votos, mas no difcil imaginar que possa crescer. +O PCP tem os mesmos sete por cento, mas quem imagina que possa subir? +Alm disso, a nova lei orgnica do LNIV veio atribuir-lhe mais responsabilidades (ver Controlo da sade animal ganha terreno, PBLICO de 9-6-97) no mbito das provas laboratoriais necessrias ao controlo sanitrio dos animais e seus produtos -- a par das normais competncias no mbito da Investigao e Desenvolvimento (I&D) e das que decorrem do seu estatuto de autoridade nacional de referncia para todas as questes referentes sade animal. +Por tudo isso, o ministro da Agricultura entendeu que no era possvel adiar por mais tempo a deciso relativamente ao futuro do laboratrio. +O despacho de Gomes da Silva aprova uma metodologia de realizao faseada no tempo. +A primeira iniciativa consiste na abertura de um concurso limitado para a elaborao, por um gabinete de projectistas, do programa preliminar e do caderno de encargos que ter de ser apresentado no concurso pblico (segunda fase) para a concepo e execuo das novas instalaes. +Este ciclo, a iniciar muito brevemente, dever ser cumprido at ao final do prximo ano. +A terceira fase do processo consistir na construo das instalaes propriamente ditas, cujo arranque ocorrer em 1999 e no dever estar concluda antes de passados trs anos. +Mais de mil casais tinham-se oferecido, at quinta-feira noite, para recolher provisoriamente refugiados, sobretudo crianas. +Mas hoje [ ontem ], quando perceberam que no era para adoptar, comearam tambm a aparecer pessoas interessadas em receber tambm as mes, explicou uma fonte do Frum Estudante, que est a organizar a Misso Crescer em Esperana. +Nas instalaes do Instituto da Juventude, o telefone no pra de tocar. +Tambm na Madeira, a delegao da Critas no Funchal tem recebido inmeras solicitaes de famlias do arquiplago dispostas a receber crianas da Bsnia. +Apesar dos muitos pedidos de adopo, as pessoas esto a ser informadas que as crianas apenas sero recohidas temporariamente, entre nove a doze meses, segundo o Jornal da Madeira. +Os lobos no tm fronteiras, para alm das que eles prprios delimitam. +Mas, um simples salto sobre a linha que divide o Parque Natural de Montesinho da Reserva Regional de Caa da Serra da Culebra, na regio espanhola de Castilla y Lon, pode significar um passo da vida para a morte. +Protegido em Portugal, o lobo considerado uma espcie cinegtica no lado de l da fronteira, onde, desde h dois anos, tem vindo a ser leiloado o seu abate e a prosperar um mercado negro em torno dos seus restos. +Curiosamente, na conferncia de imprensa em que anunciou o adeus, o primeiro-ministro no apresentou justificaes de peso para a deciso. +Decidi demitir-me hoje. +Pensei ser chegado o tempo de renovar as pessoas do Governo e comear de fresco no ano novo -- afirmou, revelando que tomara a deciso dia 1. +A justificao, para muitos analistas, pode procurar-se na frase de alvio de Murayama aps o seu copo de sak. +A impopularidade pessoal e do seu executivo tornara-se demasiada. +Eles fizeram da nossa aldeia um cemitrio, contou uma velha. +at agora, as pessoas morriam de doena ou de velhice. +Homens e ps cavavam ontem freneticamente, que j tinha passado um dia, e camies descarregavam cimento em quantidades que a aldeia, de casas de argila e telhados de chapa, nunca tinha visto. +A sportinguista Teresa Machado estabeleceu no domingo, numa pequena reunio realizada em So Jacinto, perto de Aveiro, um novo recorde nacional do disco, alcanando a marca de 65,40m. + O anterior mximo fora conseguido no mesmo local a 8 de Agosto de 1993, com 63,70m. +A sportinguista abriu a sua srie com 62,16m, subiu a seguir para 63 metros exactos e o novo mximo chegou na terceira ronda. +A acabar confirmou a sua regularidade com 61,87m, 62,84m e 63,09m. +O Sporting j ganhou (a Taa de Portugal) e j voltou a perder (com o Benfica no jogo de repetio, na quarta-feira, no Restelo, com meio plantel ainda sob os festejos do fim do jejum de 13 anos). +Mas pelas bandas de Alvalade continua ainda por deslindar o mistrio do irlands Niall Quinn, contratado num dia pelo novo Sporting de Santana Lopes e descontratado no dia seguinte com o mesmo espalhafato meditico. +S porque, de facto, o actual clube do irlands, o Manchester City, tratou o Sporting como um clube terceiro-mundista, como alegou Santana Lopes? +Ou a histria tem mais que ver com os nmeros do negcio (mais de 400 mil contos por um jogador beira dos 30 anos, indesejado pelos tcnicos de Alvalade e chumbado pelo respectivo departamento mdico)? +A ser verdade, o Sporting continua a fazer negcios Cintra. +E a portar-se Cintra, no dando cavaco, sequer, ao jogador ,que at j vendera a casa l na Old England. +Ser que est a nascer o Sporting Cavaco Lopes? +De um modo aparentemente displicente, mas evidentemente estudado nos seus efeitos, surgem referncias a Nitsch Hermann e a Rodolf Schwarzkogler, por exemplo, personagens que, com Arnulf Rainer e Gnter Brus, compuseram um grupo de artistas vienenses capaz de, nos anos 60, provocar escndalo no s na cena pantanosa e hipcrita da arte austraca do ps-guerra, como no conjunto da arte ocidental. +Radicalizando a ideia das vanguardas em reduzir cada linguagem artstica sua especificidade, eles acabaram por inverter a lgica de purificao de meios do modernismo. +O corpo humano (na sua realidade fsica) tornou-se suporte -- ou, se quisermos, a prpria matria-prima da obra de arte. +Chamaram-se a si mesmos actionistas, embora o movimento, que se espalhou largamente at aos anos 70, seja designado globalmente por body art, ou surja nos textos de Bowie como arte ritual. +Por mim, achei que foi infeliz semelhante comparao (...) e, muito menos, em relao quele grande estadista, que penso ser respeitado em todo o mundo, sempre a rigor fardado, de franciscanas e respeitveis barbas e de linguagem to simples que todo o explorado da terra entende e o povo cubano venera e ama. +Debate-se agora se ter sido oportuno ou nefasto o reconhecimento internacional da Crocia e houve quem recordasse que a guerra servo-croata j estava em curso em 1991, antes desse reconhecimento. +Mas este no o cerne da questo, o cerne da questo a Bsnia. +a sua dignidade enquanto naes, e logo enquanto Estados, estava firmada. +O mesmo no se passa na Bsnia, onde as etnias convivem ao simples nvel da coabitao. +Por isso, constituiu um erro reconhecer a Crocia sem antes ter preparado uma soluo vivel para a Bsnia, e no h paixo ideolgica capaz de o apagar, se bem que seja intil dramatizar um erro de natureza diplomtica. +A urbanizao de Arcena passou por um complicado processo, com origem numa venda entre scios da empresa proprietria, a Eurocapital. +Perante a falta de pagamento dos compradores, os scios vendedores reclamaram em tribunal a restituio dos seus direitos sobre a urbanizao, o que viriam a conseguir no incio deste ano. +Durante o perodo em que estava indefinida a propriedade dos edifcios, grande parte deles foram ocupados por imigrantes africanos. +Em 9 de Abril ltimo, o tribunal de Vila Franca iniciava aces de despejo, para a restituio da totalidade da urbanizao Eurocapital. +A maior parte das cerca de 260 pessoas desalojadas instalou ento abrigos improvisados numa das ruas da urbanizao. +A polcia alem localizou, ontem de manh, os dois criminosos que fizeram cinco refns durante uma fuga de mais de 27 horas pela Alemanha, aps terem assaltado um banco, na segunda-feira, em Stuttgart. +Quando tentavam controlar a identidade de um homem perto de uma cabina telefnica em Driedorf, em Hessen, os polcias foram alvo de tiros, tendo respondido. +O homem acabou por fugir a p. +Importantes reforos foram j enviados para a zona. +Os dois criminosos tinham libertado, ontem, por volta das 4h50 locais, os trs refns que detinham, um dos quais ferido, segundo a polcia de Wiesbaden. +Os refns, um homem e um casal, encontravam-se a cerca de 60 quilmetros a norte de Frankfurt. +Aps uma perseguio de mil quilmetros atravs do pas, a polcia a distribuu-se pela autoestrada entre Hofheim e Wiesbade. +Os agentes decidiram dar vantagem aos raptores por estes terem ameaado suicidar-se com os refns fazendo explodir uma granada, caso a polcia os seguisse a curta distncia. +Os delinquentes comearam por sequestrar dois polcias no seguimento do assalto ao banco que lhes rendeu 250 mil marcos, tendo feito depois trs novos refns. +Os dois criminosos, um antigo soldado de elite do exrcito da Alemanha de Leste de 32 anos, e um suo de 35, evadiram-se, h trs semanas, de uma priso de Hamburgo, acusados de homicdio e assalto mo armada. +Segundo as autoridades, no est excluda a possibilidade de que os raptores se dirijam para Frana. +O prmio literrio Donna -Cidade de Roma, 1992, foi atribudo a Raisa Gorbatchov, pela sua biografia-testemunho, Io Spero, editada no Vero passado, anunciou ontem, na capital italiana, a presidente do jri, a escritora Gabriella Sobrino. +O galardo ser entregue numa cerimnia a decorrer a 28 de Maro, mas ainda no est confirmada a deslocao de Raisa a Itlia. +Telas atribudas a Picasso, Mir e Martinez foram descobertas ontem, pela polcia Judiciria de Paris, na casa de um galerista de origem americana, Theodor Robertson. +A descoberta aconteceu na sequncia de uma busca efectuada sua residncia, em Frana. +Robertson era alvo de um mandato internacional de captura, por acusaes de fraude e falsificao. +Nove emprstimos por obrigaes e as aces da Luzostela foram ontem retirados da cotao na Bolsa de Lisboa. +Dificuldades financeiras so o motivo invocado para a medida. +Os responsveis do mercado lisboeta decidiram tambm extinguir a direco de operaes. +A reduzida actividade do floor j no justificava a sua existncia. +O escndalo Augusta explodiu no seguimento das investigaes sobre o assassinato, em 1991, de um antigo ministro socialista francfono, Andr Cools. +De acordo com Philippe Moureaux, ex-vice-primeiro-ministro do mesmo partido, a sua morte ocorreu no momento em que Cools estaria prestes a revelar o pagamento das luvas pela firma italiana. +em Janeiro de 1994, os trs Guys -- Guy Spitaels, ministro-presidente da Valnia, Guy Coeme, ministro da Defesa, e Guy Mathot, ministro regional -- foram forados a demitir-se para assegurar a sua defesa depois de a sua imunidade parlamentar ter sido suspensa. +Neste caso, h duas coisas que custam sobremaneira a Fernando Neves. +Primeiro: o processo coloca-o na ingrata posio de delator dos seus camaradas. +Segundo: a queixa foi apresentada pelo governador civil, um homem que Neves chegou a louvar pela sua disponibilidade para o dilogo, que contrastou com o silncio comprometido do presidente da Cmara de Castelo de Paiva, Antero Gaspar. +No estava nada espera disto, diz o mineiro. +Entretanto, por causa deste processo, a agitao ameaa voltar ao Pejo. +Os mineiros esto dispostos a manifestar-se na sede do concelho em solidariedade com o seu camarada, ao mesmo tempo que est a ser preparado um abaixo-assinado. +Se querem prender algum, tm que nos prender a todos, porque fomos ns todos juntos que cortmos a estrada e barricmos a empresa. +Sempre quero ver se tm prises que cheguem para l meter dentro 500 mineiros!, diz Manuel Vasconcelos. +O mdico Manuel Almeida Marta, principal suspeito do crime de Armamar, continua a ser procurado pelas autoridades. +As buscas duram h 72 horas, sem que tenha sido descoberto o paradeiro do mdico que por pouco escapou a um linchamento popular. +Muito mais no diz, nem vale a pena, apenas um sussurro curto de remorso. +Estou arrependido, mas no tinha comido nada. +O Tribunal de Polcia entende as palavras de Agostinho como pesar sincero, e esta uma pequena condio muito prezada pela letra e forma da lei. +Mas uma atenuante no um perdo, e os cdigos esticam as penas em caso de reincidncia. +Ainda se considera, pela ltima vez, ser desnecessria a priso, dita o juiz para o escrivo, convertendo os cinco meses na penitenciria em 45 mil escudos de multa, ou, em alternativa, em cem dias de deteno. +Atende-se, tambm, situao econmica do arguido e autoriza-se o pagamento da multa em dez prestaes. +Quanto ao carvo, que j regressara ao restaurante quando Agostinho saiu do tribunal, foi grelhar carne para quem a come. +Esta foi a argumentao apresentada na reunio de Cmara, que mereceu, invariavelmente, alguma diplomacia na resposta dada pelo presidente. +Disse vezes sem conta Manuel Machado que, no que for vlido e legtimo, a edilidade est disposta a encontrar uma alternativa possvel. +E no ajuda muito tcnicas de bloqueios a mquinas. +Se for a bem, tudo bem; se for a mal, arranja-se a um '31' que s o tribunal resolve. + prefervel concertao de solues, advertiu. +Mas se a alternativa for a estrada, no possvel. +por falar em alcatro, o presidente relembrou aos moradores que, por altura das campanha para as eleies autquicas de 1993, lhe tinha sido pedido alcatro para o local. +Constata-se que nem sempre, na altura prpria, os muncipes do ateno aos editais. +Porque nenhum loteamento avana em Coimbra sem publicao de editais. +No falem em falta de dilogo. +Nenhuma audincia dos moradores foi pedida Cmara. +Foi apenas recebido um fax da CHEM a mandar parar as obras, mas no se pode retomar o PREC numa situao desta natureza. +Os dois principais partidos austracos chegaram ontem a um acordo para fazer redues oramentais no valor de 100 mil milhes de xelins (perto de 1500 milhes de contos) em 1996 e 1997. +Dois teros dessas economias sero obtidas atravs de cortes nas despesas do Estado Federal, provncias e autoridades comunais, estando o resto dependente de um aumento de receitas. +As medidas, anunciadas pelos ministros da Economia, Johannes Ditz (conservador), e das Finanas, Viktor Klima (social-democrata), numa conferncia de imprensa comum, destinam-se a assegurar o cumprimento dos critrios de convergncia, assegurando que a Astria possa entrar no primeiro comboio da Unio Econmica e Monetria. +Sem estas redues, o dfice pblico representaria 6,1 por cento do Produto Interno Bruto (PIB) este ano e 6,5 em 1997, valores muito acima do limite de trs por cento imposto por Maastricht. +PIB desce em Frana ... +O PP decidiu ontem em reunio do grupo parlamentar adoptar a liberdade de voto e Helena Santo ficou -vontade perante o debate parlamentar da prxima quinta-feira em que sero votados os projectos de alterao e liberalizao do aborto. +A deputada no decidiu ainda, contudo, se se vai abster ou votar a favor do texto de Strecht Monteiro, mas assume claramente que considera o projecto pr-natalista e vem corrigir a lei em vigor, pelo que merece simpatia. +Este afirmou que o seu objectivo devolver CIA a credibilidade perdida. +Precisou que ia realizar mudanas profundas na gesto da CIA, designadamente em o que respeita aos critrios aplicados no recrutamento de agentes. +Os EUA tm de ter a melhor capacidade do mundo para recolher informaes, concluiu. +Aldricht Ames, aps ter sido descoberto, confessou que espiava a favor de Moscovo havia nove anos, desde Abril de 1985. +Devido aco de Ames, explicou o actual director da CIA, foi muito mais difcil para os EUA compreender o que se passava na URSS durante aquele perodo crtico, porque ele denunciou aos soviticos muitos agentes que trabalhavam para os servios americanos. +Depois de Cantanhede, a formao apresenta-se hoje em Vila Real, no sbado no Porto e na prxima quinta-feira em Tomar (ver pgs. 10/11 deste Fim de Semana). +At 24 de Outubro, a Nova Filarmonia -- que actualmente composta por 35 msicos -- estar em Covilh, Leiria, Paio Pires, Lisboa, Coimbra, Valena, volta a Vila Real e, finalmente, toca em Matosinhos. +Entre os variadssimos patrocinadores destes concertos, encontram-se, entre outras empresas e instituies financeiras, a Rodoviria Nacional, Portucel, Shell Portuguesa, Sonae, Banco Totta & Aores e Montepio Geral. +Entre os concertos previstos para Novembro, destaca-se o de dia 3, no Palcio Nacional de Queluz, quando a orquestra acompanhar o pianista Sequeira Costa, que interpretar o concerto n 1, opus 11, de Chopin, com o patrocnio da GALP. +O rio Grande nasce no concelho de Torres Vedras e atravessa todo o concelho da Lourinh sendo as suas guas consideradas excessivamente poludas e imprprias para qualquer uso, segundo dados de 1987 da Direco Geral da Qualidade do Ambiente. +Neste estudo so apontados como principais fontes poluidores as pecurias, matadouros e esgotos domsticos sendo observados vrios casos de contaminao de guas de fontanrios e poos. + sua poluio deve-se ainda o desaparecimento dos famosos bivalves negros da Mexelhoeira, uma zona de rochas entre as praias da Areia Branca e do Peralta. +As histrias da poluio do rio Grande correm toda a regio, desde o aparecimento de cadveres de animais na sua foz at ao boato de um surto de hepatite B que no ano passado afastou centenas de veraneantes. +Na perspectiva da autarquia este quadro est em vias de melhorar pois existem j projectos para cinco estaes de tratamento domsticos das principais localidades do concelho e duas centrais de tratamento de dejectos das suiniculturas para posterior transformao em fertilizante, j candidatas a fundos comunitrios, isto para alm das vrias pequenas estaes de tratamento que esto previstas em todo o concelho. +A Cmara Municipal aponta os complicados processos burocrticos como os grandes entraves para que tudo se concretize uma vez que a maioria dos apoios financeiros j estar garantida. +A ajuda humanitria, que finalmente recomeou a mover-se em direco Bsnia central, pode revelar-se uma arma de dois gumes. +Adia o sofrimento de uma populao esgotada e esfomeada, mas no o resolve de vez. +Crescem os receios de que antes contribua para prolongar uma guerra devastadora e alimentar os florescentes circuitos do mercado negro. +Quanto ao projecto paisagstico, as dvidas persistem. +Armindo Cordeiro, da Cmara Municipal de Lisboa, afirma ainda no ter sido aprovado nenhum, mas que tal vir a acontecer a breve trecho, cabendo a responsabilidade ao Departamento de Espaos Verves, j que o projecto apresentado pela Junta de Freguesia foi recusado poe no reunir os requisitos de qualidade necessrios. +Tambm Armindo Cordeiro peremptrio ao afirmar que o logradouro ajardinado no ser destrudo, mas pelo contrrio ampliado, assim como melhorar a situao dos moradores e lojistas da zona. +Acabar com o desleixo e a degradao do parque actualmente existente, que no tem cuidados de jardinagem h muito tempo e que poder transformar-se em poiso de marginais e toxicodependentes. +Ablio Loureno, da Comisso de Luta dos Professores de Educao Tecnolgica (Colpet), lembra que a anterior tutela se limitou a reconverter os professores de Trabalhos Oficinais em professores de Educao Tecnolgica, sem lhes dar qualquer tipo de formao. +as professoras de Txteis s ensinam Txteis, os professores de Electrotecnia s do a sua matria e por a adiante. +Nas aces de formao que a Colpet tem vindo a organizar, ficou provado que os docentes no cumprem os novos programas porque no lhes foi dada a necessria reciclagem. +Como s dominam uma das matrias do programa, quando so confrontados com o enunciado de uma Prova Global de Educao Tecnolgica, os professores, na generalidade, no sabem resolv-la. +A rapariga franzina faz o que pode, mas a voz to titubeante que o apresentador tem logo que ir buscar outra cantora multido. +Percebe-se que a grande massa de gente est ali mais ou menos como o Padro dos Descobrimentos: para proporcionar SIC o seu show de iluso, a ideia de que uma multido entusistica rodeia o palco, o que serve s mil-maravilhas os primeiros planos das cmaras. +As coisas animam-se decididamente com Crocodile rock de Elton John. +Os cantores sucedem-se, Miguel ngelo j pula, por momentos dana Elvis e pe o concorrente a danar. +Quantas Laura Pausini esto aqui esta noite?, pergunta. +As duas primeiras jovens fraquejam e Miguel vai logo buscar outra. +O novo parque urbano de Ermesinde, que comea a ganhar forma junto s runas da antiga Fbrica da Cermica (vulgo Fbrica da Telha), ficar pronto em finais deste ms. + esta a expectativa assumida pelo presidente da Cmara de Valongo, Fernando Melo, empenhado em abrir aos ermesindenses a primeira fase deste novo equipamento de passeio e lazer antes que se escoe o presente mandato autrquico. +Como o PBLICO pde constatar no local, a ideia transformar, nesse curto espao de tempo, um terreno que ainda se encontra revolvido e enlameado numa rea relvada cruzada por passeios e estadias. +J podem ser vistas algumas das estruturas que vo integrar o parque, despontando no meio de um piso em rebulio. +So os casos de alguns dos caminhos e do pequeno anfiteatro ao ar livre destinado a espectculos estivais. +Hoje e amanh, decorrem na Sala Polivalente do Acarte as ltimas de Josa com o Violino Mgico dos London Puppet Players. +Porto: no Carlos Alberto, continua Luzes de Palco, a ltima produo da Seiva Trupe . +Que futuro para o livro na Europa o mote que preside a um frum internacional que a Associao Portuguesa de Editores e Livreiros (APEL) organiza, em 16 e 17 de Fevereiro, no Centro Cultural de Belm, em Lisboa. +Para alm de representaes de vrias estruturas associativas europeias de editores e livreiros, est prevista a presena do comissrio europeu com a pasta da Cultura, Martin Bangemann, bem como de vrios membros do Parlamento Europeu. +A discusso centrar-se- em trs grandes painis: a edio na sociedade europeia de informao, a dimenso cultural no Tratado da Unio Europeia e o mercado do livro na Europa: problemas de comercializao (preo fixo). +O ministro da Cultura portugus, Manuel Maria Carrilho, encerra o debate. +P. -- Qual a diferena entre trabalhar em teatro e em cinema? +R. -- No teatro tenta-se encontrar a unidade do espectculo, cria-se um tempo, e no um momento -- cria-se um tempo fora do tempo. +Em o cinema, essencialmente o que se apanha o momento de qualquer coisa. + um trabalho sobre o detalhe. +Foi em 1913 que a aventura teve incio. +Jesse Lasky, a conselho de Sam Goldfish (depois Goldowyn) d a primeira oportunidade a DeMille para fazer um filme: The Squaw Man. +O palco das filmagens seria New Jersey. +Os sicrios do fara Edison, cujo trust controlava as patentes das mquinas de filmar foraram o grande xodo. +Ei-lo de abalada para Flagstad, Arizona, com mais quatro cmplices, entre eles o actor Dustin Farnum. +Desiludido com o local, DeMille segue mais para Oeste (seguindo, como os pioneiros, as palavras de Horace Greeley: Go west young man, and grew up with the country ) e assenta arraiais num velho celeiro comprado numa zona quase deserta: Hollywood. +Se outros j por l andavam, fugidos ira faranica, a partir de ento que a zona comea o seu boom, tornando-se, em poucos anos, a capital dos sonhos. +Nascia a nova Terra Prometida. +O filme foi um xito e DeMille at ao fim da primeira grande guerra dirigiu e supervisou mais de metade dos filmes da sua carreira. +Desde logo se manifestou a habilidade do regisseur controlando mais de um filme em simultneo. + paradigmtico o caso de A Marca de Fogo (The Cheat), esse filme que marcou a crtica francesa e o cinema de vanguarda dos anos 20, que fez dele a sua bandeira e deu ao cinema a categoria de arte (a stima). +A Marca de Fogo foi filmado em 1914 em simultneo com The Golden Chance, o primeiro de dia e o segundo de noite. +Estreado um ano antes de Nascimento de Uma Nao, A Marca de Fogo uma obra surpreendente por tudo o que trouxe de novo em termos de linguagem cinematogrfica, nos enquadramentos e na iluminao de interiores. +Mas o programa cultural abre logo s 22h00 do dia 3 de Julho, uma sexta-feira, com a actuao da Orquestra Ligeira do Conservatrio de Coimbra, na Praa do Comrcio, e encerra no mesmo local e mesma hora no domingo, dia 12, com os Cool Hipnoise. +Este mesmo espao, no corao da cidade, acolhe ao longo da semana e sempre mesma hora um espectculo de msica e dana sevilhana (dia 6), o grupo Negros de Luz (dia 7), Fausto (dia 8), Cesria vora (dia 10) e Carlos do Carmo (dia 11). +Muito perto, nas principais ruas da Baixa de Coimbra, a animao ser constante, garante a organizao. +Aos saltimbancos, bombos e gigantones juntar-se-o, ao longo dos vrios dias de festa, a msica, o malabarismo, animaes de fogo, teatro e fantoches. +P. -- H algo de verdadeiramente diferente em Where In The World, o seu ltimo trabalho? +R. -- Penso que o fundamental que Where In The World o primeiro lbum mesmo da banda, s isso. +O lbum funciona mais como uma afirmao, feito como um todo, no soa como trs ou quatro discos diferentes. +No se construiu um mundo diferente para cada um dos seus temas, em estdio. +quando, finalmente, conseguimos um disco realmente da banda, ficamos sem um dos msicos. +A directoria de Faro da Polcia Judiciria encerrou na passada quarta-feira, aps denncia, e na sequncia de investigaes que estavam a ser feitas no Algarve sobre o aliciamento de mulheres para a prostituio, um bordel na zona de Ferreiras. +Na operao foram detectadas, entre outras, quatro jovens com idades compreendidas entre os 16 e os 18 anos, que eram foradas, segundo a PJ, a exercer a prostituio. +O PSD decidiu deixar hoje pronta a Lei da Greve. +Para isso, depois do debate na generalidade agendado para hoje, pedir a avocao dos artigos do seu projecto-lei, de maneira a deixar feita a discusso na especialidade, despachando assim esta questo antes de frias. +Em protesto contra esta pressa social-democrata, que deitar por terra o projecto do CDS, a oposio decidiu no dar acordo para que a Lei Orgnica do Ministrio Pblico, que regressou ao Parlamento ferida de inconstitucionalidades, seja expurgada antes do prazo regimental. +Assim, a aprovao desta lei, prevista para amanh, obrigar realizao de uma sesso extraordinria, j marcada para a prxima quarta-feira. +Um lder sempre tem os seus fiis. +Mas, com a normal tendncia para a reduo da realidade, passou-se a falar no apenas na Escola do Porto, conceito com origem numa falcia regionalista j muito afastada, mas numa j mesquinha e inexistente Escola do Siza. +A arquitectura de Siza Vieira no o permite, talvez porque em todo o lado seja prprio da liderana cultural, o no se submeter s regras que ela prpria cria. +Como estilo que nunca se chega a definir, como caminho que est sempre interminado, a obra do mestre mais a permanente inflexo que a coerncia inabalvel. +Apaixonei-me imediatamente pela Casa dos Espritos. +Telefonei mil vezes para a agente de Isabel Allende, e finalmente consegui, isso foi nos Estados Unidos, onde forcei Isabel a ver Pelle, o Conquistador antes da estreia na Dinamarca. +Isabel respondeu-me que ia pensar na proposta. +No dia seguinte ligou para mim, e disse que estava interessada. +este filme mesmo internacional. +Mas a adaptao ao cinema foi fcil. +A minha ambio era a de contar exactamente a mesma histria da de Isabel. +Depois tive que encontrar os melhores actores que h neste mundo, o que tambm foi muito complicado e fascinante, e eu acho, sinceramente, que conseguimos criar uma famlia credvel para a Casa dos Espritos. +Optvamos por filmar em Portugal porque era demasiado difcil no Chile, onde as autoridades no vem com bons olhos, ainda hoje, uma recriao cinematogrfica do golpe de estado contra Salvador Allende. +mas sairia muito caro. +E devo dizer que a parte portuguesa da nossa grande equipa tem sido eficaz e prestvel. +As autoridades portuguesas, entre elas os militares, tm sido muito abertas, concluiu Bille August que ainda revelou que A Casa dos Espritos ter a sua primeira estreia na Alemanha em Outubro deste ano. +Calmamente, sem grandes foguetes de marketing, acaba de acontecer um daqueles pequenos passos na informtica que podem significar uma grande revoluo para o mundo da comunicao tal como o conhecemos. +Trata-se da aliana entre a Adobe, a mais importante empresa no ramo da concepo de produtos para edio electrnica e a Netscape, que em seis meses capturou 75 por cento do mercado de programas de navegao na Internet com o produto que lhe deu o nome. +Msica irlandesa em vora: os Wingers tocam na Feira de So Joo, naquela cidade. +s 21 h. +No mbito do Ciclo Jovens Compositores, organizado pelo Acarte da Fundao Gulbenkian, pode escutar-se a obra ...H Dois Ou ..., de Antnio de Sousa Dias. +Os intrpretes so Joo Natividade (movimento), Lus Madureira (voz), Olga Pratts (piano), Pedro Wallenstein (contrabaixo), Antnio de Sousa Dias (percusso) e Clemente Cuba (desenho de luzes). +s 21h30. +A atleta e o seu tcnico, Joo Campos, elegeram a corrida de 10.000m como um meio de ela se sagrar campe olmpica. +Apesar de ser a recordista mundial dos 5000m, nos 10.000m que a Fernanda se exprime melhor, considera Campos. +Fernanda Ribeiro no se aterroriza sequer com as condies climatricas que, decerto, ir encontrar -- muito calor e humidade. +O clima igual para todos e no me preocupa muito. +Iremos com antecedncia para nos adaptarmos. +As mais temveis devero ser as do costume, em especial a etope Tulu. +No Word, o salto da verso 2.0c para a 6.0 significou uma alterao profunda na orientao do programa. +Em vez de acrescentar uma infinidade de caractersticas novas, o novo Word privilegiou a consistncia de funcionamento e a facilidade de acesso e de aprendizagem. +Um dos objectivos do novo Word foi tambm assegurar a mxima coerncia entre as verses para Windows e para Macintosh. +Desde o manual (que o mesmo para os dois produtos) ao formato de ficheiros, tudo dever contribuir para que a mudana entre estas duas plataformas no oferea qualquer problema. +por exemplo, o excelente search and replace tem caractersticas que s existiam na verso 5.1 para Mac. +O WordPerfect, por seu lado, ao passar de 5.2 para 6 tornou-se praticamente num novo produto. +Foi, dos trs, o que teve a alterao mais radical e, por isso, o que apresenta a maior quota-parte de problemas. +Quando as imagens do Cobe foram publicadas, elas emocionaram toda a gente. +Afinal elas representavam o embrio do nosso Universo. +Um embrio to primitivo que quase parecia impossvel recuar mais no tempo. +No entanto, Smoot pensa que vai ser possvel ir ainda mais longe. +Penso que possvel recuar ainda mais no tempo, diz o astrofsico. +Acho que vai ser possvel aprender algo mais sobre a inflao do Universo e sobre o seu princpio. +Acho que mesmo possvel chegar at ao Big Bang. +Em Coimbra, os estudantes marcaram um golo defesa menos batida da prova, marca suficiente para alcanar o triunfo e justificar a recuperao das ltimas jornadas, que j levou a equipa a ascender ao quarto lugar na competio aps um mau incio de poca. +A liderana agora repartida entre Tirsense e Rio Ave, ambos com onze pontos, contra dez do Campomaiorense, que foi empatar (2-2), no sbado, Madeira, frente ao Nacional, e da Acadmica. +Logo atrs, surge o surpreendente Desportivo das Aves, que foi golear fora o Sporting de Espinho, por 3-0, e a Ovarense -- empate a duas bolas no Algarve com o Louletano --, ambos com nove pontos. +A diferena mnima entre os primeiros reflecte uma vez mais o equilbrio da prova, tanto mais que o primeiro e o dcimo quarto da classificao geral esto separados somente por quatro pontos. +Referncia maior para o Espinho, que, depois de, na temporada passada, ter disputado a I Diviso, est a ter um comportamento desastroso, pois o lanterna vermelha. +Cinco pontos em oito jogos muito pouco para um clube que procura o regresso prova maior do futebol portugus. +Entretanto, os espinhenses disfrutam da companhia do Penafiel, que foi perder a Viseu por 3-1 e est a repetir a m campanha da temporada anterior. +Nos restantes encontros, o Torrense foi garantir o empate (0-0) em Matosinhos perante o Leixes, enquanto as vitrias tangenciais (1-0) do Unio de Leiria sobre o Lea e do Chaves contra o Felgueiras, vieram elevar para 22 os golos marcados nesta 8 jornada, que forneceu quatro vitrias dos visitados, quatro empates e apenas uma vitria fora. +O escudo esteve bastante pressionado, devido sobretudo a vendas especulativas por parte de bancos estrangeiros, e as frequentes intervenes do Banco de Portugal apresentaram-se eficazes, quebrando a tendncia de queda, por vezes acentuada, da divisa nacional. + R. -- Era o meu ano de agregao em Filosofia; conclui-a nesse ms na Sorbonne comentando uma frase de Einstein sobre a compreensibilidade do mundo. +Mas nunca fui um dirigente do movimento, apenas um simples peo. +Tenho 46 anos, ou seja, perteno exactamente a essa gerao, facto de que me orgulho muito. +Penso que em relao s minhas filhas, que tm hoje 23 e 20 anos, tive muito mais sorte, apesar de ter crescido numa sociedade que era certamente mais autoritria que a delas. +As relaes entre pais e filhos so hoje de melhor qualidade. +P. -- As suas filhas compreendem o que se passou em Maio 68? +Compreendem o que que vocs queriam? +No momento em que a Unio Europeia decidiu abandonar a explorao do carvo de pedra, existem cinco mil mineiros portugueses no Norte de Espanha, nas provncias de Len e das Astrias, condenados a assistir ao encerramento das minas onde trabalham. +Partiram para um El Dorado, deparam-se agora com a perspectiva de uma reforma antecipada ou do desemprego forado. +A UE o principal parceiro comercial da Rssia, representando 37 por cento do total das trocas comerciais contra 24 por cento no caso das restantes ex-repblicas soviticas, ou quatro por cento com os Estados Unidos. +Com exportaes para a UE de 15,5 mil milhes de ecus e importando o equivalente a 11,5 mil milhes, a Rssia mantinha em 1993 um excedente comercial, face aos europeus, de quatro mil milhes de ecus. +No de estranhar, porque entre os comensais reunidos no dia 29 de Agosto em casa do romancista norte-americano William Styron, prmio Pulitzer, em Martha's Vineyard, uma ilha ao sul de Boston, Massachusetts, no se encontrava apenas o Presidente dos Estados Unidos, mas sobretudo esses dois grandes obcecados dos livros que so o Nobel colombiano Gabriel Garca Mrquez e o mexicano Carlos Fuentes, ltimo prmio Prncipe das Astrias de Literatura. +Toda a gente acreditou que essa reunio, realizada em plena crise dos balseros cubanos, tinha sido programada para falar da nova crise aberta entre Cuba e os Estados Unidos e, dada a personalidade dos convivas -- Garca Mrquez tem uma relao estreita com Castro e Fuentes defende o fim do embargo norte-americano para que se inicie uma nova etapa no longo contencioso da ilha caraba com o seu poderoso vizinho --, deu-se como ponto assente que Cuba tinha de ter sido o assunto. +O carro est bem equilibrado e estamos esperanados para a corrida. +Confesso que entrei depressa demais. +O carro atravessou-se, primeiro para a direita e depois para a esquerda, o que me levou a tirar o p do acelerador. +Quanto a Pedro Matos Chaves, o piloto portugus esteve uns furos abaixo do habitual, no conseguindo com o seu BMW Srie 3 melhor que o oitavo tempo, com 1m14,667s, o que o deixou fora da super pole. +O carro alemo teimou em no se adaptar ao traado do circuito, o que deixou Chaves algo desalentado, at porque experimentava em Barcelona algumas novidades aerodinmicas que deveriam melhorar as performances do seu carro. +So jovens, de facto. +Tambm por isso, na FPJ pensa-se j em Sydney. +E espera-se o mesmo de sempre. +Que para o prximo ciclo olmpico o projecto no seja para dois, mas para quatro anos. +P. -- Qual foi a opinio que j exprimiu aos parceiros (e ao Governo) sobre a concertao estratgica proposta pelo Plano Cavaco? +R. -- H muito que defendo que os acordos de concertao social devem ultrapassar as dimenses temporal e de contedo que tm tido. +Os acordos de concertao social tm vigorado sempre por um ano. +Agora temos uma proposta, inovadora, de mdio prazo, ajustada a todo este ciclo que o pas vai atravessar de 1995 a 1999. +Parece-me lgico ligar um acordo de concertao social ao Quadro Comunitrio de Apoio (QCA) e ao PDR, na vertente especfica de um acordo de concertao social e, mais do que isso, porque o primeiro-ministro afirmou aqui, na reunio, que est disposto a que o prprio acordo contemple medidas de acompanhamento e de avaliao do QCA e do PDR, visto que pode acontecer que os cenrios macro-econmicos e macro-sociais se alterem face ao previsto no PDR e no QCA. +E essas alteraes devero ser discutidas com os parceiros sociais e no apenas decididas pelo Governo. +Dois empresrios, um chins de Hong Kong e outro portugus, encontram-se para falar de negcios. +O primeiro comea por explicar que est ligado construo de um prdio para habitao e que tem tido dificuldades em encontrar uma fornecedora de material sanitrio nas medidas exigidas. +Porqu? +Porque a exiguidade de espao em Hong Kong tal que at um metro quadrado de um apartamento para habitao social custa ouro, como quem diz, entre 600 e 800 contos. +Para que se aproveite o espao ao mximo, h zonas que tm de ser encolhidas, o que acontece com a casa de banho. +Por isso, em Hong Kong as medidas-padro ficam muitos centmetros abaixo. +Ao princpio, quando vi ali o carro, sempre pensei que fosse o engenheiro que vinha vistoriar a obra. +Depois, como ele nunca mais saa, comecei a espreitar. +Foi ento que o vi tirar a boina e umas barbas pretas, contou Jos Lopes. +O pedreiro adiantou que ainda esteve tentado a dirigir-se ao carro e dizer ao fulano que o Carnaval j acabou. +S no o fez porque o empreiteiro da obra o demoveu. +Mais tarde, o Ford Fiesta voltou a parar no mesmo local -- esquina da rua da Penso Beira Baixa, onde Carlos Miguel almoou, e tambm muito prximo de um eucaliptal onde o rapto se consumou. +S que, desta feita, o homem, em vez de a barba, tinha bigode, a boina desaparecera e trazia culos escuros. +Jos Lopes garante ser o mesmo que vira durante a manh e que estava a fazer uma chamada atravs de um telemvel. +O estudante nunca falsifica uma assinatura, deixa descansar o seu encarregado de educao, que v em ele um grande futuro. +O estudante no bebe, saboreia. +Brennan e Laurent Filipe revelam uma maior segurana instrumental, a bateria de Accio Salero (visiense apostado em desmentir a macrocefalia nacional que no jazz pouco menos do que ditatorial) mantm um grau de ateno ao que se passa sua volta absolutamente indispensvel afirmao de qualquer bom jazzman, e o contrabaixo de David Gausden, embora fraquejando quando lhe compete marcar o sentido da marcha, cumpre razoavelmente a sua funo colectiva. +Uma palavra final para sublinhar trs elementos essenciais, clara e inteligente -mente valorizados por Patrick Brennan: o prazer e a alegria de tocar (sem os quais, alis, a msica de Monk inacessvel), relembrando que, mais do que um emprego, o palco um local de paixo; o peso do humor no desenvolvimento das notas e, acima de tudo, a sua presena peripattica, mesmo quando instrumentalmente afastado do discurso, danando os silncios e coreografando os tempos. + esse um dos grandes segredos da msica do pianista (cujos bailados foram, quase sempre, interpretados como uma mera excentricidade exibicionista). + que a danar que Monk se entende. +O croata Goran Ivanisevic, o norte-americano John McEnroe, o checoslovaco Petr Korda, o francs Henri Leconte e o holands Richard Krajicek foram os primeiros a assegurar a passagem segunda ronda da Taa do Grand Slam em tnis, competio que est a ser disputada em Munique (Alemanha) e que est dotada com seis milhes de dlares (cerca de 840 mil contos) em prmios. +Ivanisevic, n 4 do ranking mundial, encontrou algumas dificuldades para bater o francs Guy Forget (7-5 e 6-4), acabando por se impr na batalha do servio. +O francs conseguiu dez ases no encontro, contra 17 do croata, que est perto de atingir a incrvel marca de mil ases este ano. +Ivanisevic vai defrontar na segunda ronda o norte-americano John McEnroe, que eliminou o sueco Niklas Kulti (6-1 e 6-4). +McEnroe, que nas ltimas semanas tem evitado os jornalistas, depois de ter admitido dificuldades no seu casamento com a actriz Tatum O'Neal, voltou a jogar o seu melhor tnis, empolgando a assistncia com todo o seu repertrio de pancadas espectaculares. +Henri Leconte, que venceu o sul-africano Wayne Ferreira (3-6, 6-3 e 6-0), vai defrontar na prxima ronda o vencedor do encontro entre Pete Sampras (EUA) e Alexander Volkov (Rssia). +Tambm Petr Korda no encontrou facilidades para derrotar o australiano Wally Mansur (2-6, 7-5 e 6-4), esperando agora pelo norte-americano Michael Chang, que bateu o seu compatriota Andr Agassi (6-4 e 6-2). +Por fim, o jovem holands Richard Krajicek teve o encontro mais fcil desta primeira ronda, batendo o espanhol Emlio Sanchez (especialista em pisos mais lentos) por 6-3 e 6-2. +Na prxima jornada, Krajicek defrontar o vencedor do encontro entre Stefan Edberg (Sucia) e Michael Stich (Alemanha). +Eles representam mais um aborrecimento do que uma ameaa para o Governo, comentou Sergio Ramirez, o sandinista que preside ao Parlamento. +O perigo a internacionalizao do conflito, por causa da instabilidade que os grupos armados causam nas zonas onde operam. +Tony Bryant, lder dos Comandos L, uma organizao hostil ao regime de Fidel Castro e sediada em Miami, admitiu ao Washington Post que est a ajudar os Recontras na luta contra Violeta Chamorro e os sandinistas. +O corpo do escritor francs Alain Fournier, autor de O Grande Meaulnes, morto no comeo da I Guerra Mundial, foi formalmente identificado, anunciou ontem a Direco Regional de Assuntos Culturais Franceses, na cidade de Metz. +Os despojos do escritor francs era um dos 19 cadveres descobertos em Novembro, na regio de Verdun, local da batalha contra o exrcito alemo onde se sabia que Alain Fournier tinha sucumbido. +O autor de O Grande Meaulnes (edio portuguesa na Relgio d'gua) pertencia ao 288 Regimento de Infantaria que a combateu. +O seu corpo foi identificado por antrometria, sem qualquer margem para dvidas, pela equipa chefiada pelo antroplogo Fredric Adam, que comprovou a estatura do esqueleto, a sua idade data da morte e a compleio fsica, por comparao com documentos da poca. +Contra a lenda que falava da morte por tortura do escritor, s mos de soldados alemes, os exames efectuados por Adam demonstram que a morte de Henri Alban Fournier, dito Alain Fournier, se deveu a impactos de balas. +Foi em 22 de Setembro de 1914. +R. -- No, de todo. +O que acontece que, na altura em que foram feitas as estimativas das receitas no oramento, era j sabido que o cenrio internacional adiantado pelo Fundo Monetrio Internacional poderia ser optimista. +Isso implicou uma maior incerteza e estimativas acrescidamente prudentes. +A confirmao de que as perspectivas de crescimento no so, afinal, to favorveis s veio validar essa abordagem. +P. -- Quer dizer que nunca se acreditou num crescimento de trs por cento e que a estimativa da receita foi feita, desde o incio, a pensar em dois por cento ou menos? +Os outros operadores so os membros negociadores (intervm na negociao em bolsa, introduzindo directamente no sistema ofertas de compra ou de venda) e os membros compensadores, que alm de as funes dos anteriores, participam, designadamente, nos procedimentos de liquidao de contratos nas datas de vencimento e no processo de constituio, substituio, reduo e libertao de garantias. +O futuro sobre OT-10 (uma taxa de juro de longo prazo) conta, partida, com nove market makers, significativamente mais do que os cinco que cumpriro indntica funo para o futuro sobre o PSI-20 (um ndice bolsista calculado para uma carteira de 20 ttulos). +O contrato sobre a Lisbor a trs meses (taxa de juro de curto prazo, correspondente mdia das taxas praticadas no mercado monetrio interbancrio) s comear a ser negociado no incio de Julho mas j tem garantidos seis market makers. +Segundo o Instituto Nacional de Estatstica (INE), Portugal exportou 265,2 milhes de contos e importou 521 milhes de contos, durante os primeiros cinco meses de 1995, o que resultou num dfice comercial de 255,8 milhes de contos. +A taxa de cobertura das importaes pelas exportaes melhorou, situando-se em 50,9 por cento, contra 45,1 por cento nos cinco primeiros meses de 1994. +Em Maio verificou-se um aumento de 19,3 por cento nas exportaes e uma queda de 4,3 por cento nas importaes, o que permitiu uma diminuio de 19,2 por cento no dfice comercial mensal, em relao ao mesmo ms de 1994. +As compras aos PALOP (Pases Africanos de Lngua Oficial Portuguesa) aumentaram 45,5 por cento de Janeiro a Maio deste ano, face a idntico perodo de 1994, enquanto as vendas ao Japo subiram 34,6 por cento no mesmo perodo. +O caso tem sido absolutamente devastador para a agncia .. tanto em termos de pblico como internamente, disse um funcionrio da Administrao. +Temos de o ultrapassar ...a fim de restaurar a confiana do Congresso e do pblico. +Creio que era tempo de Woolsey sair. +E deu como motivos ele no haver despedido quadros que no conseguiram detectar a tempo o trabalho de sapa que Aldrich Ames andava a realizar. +A banda sonora de Thirtysomething a de uma srie televisiva, domnio onde a msica vem sendo tratada pior que no cinema. +Claro que tambm faz os usuais recursos a fundo de catlogo, mas logo a esse nvel h uma selectividade fora do comum, e os temas repescados de Ray Charles como de Rickie Lee Jones so no apenas canes clssicas, como concordam absolutamente com o esprito da srie. +Mais importante que isso, a msica composta por Snuffy Walden no um mero adereo, mas surge como outro personagem de Thirtysomething, de algum modo participando do seu enredo e da sua lgica, mas com a diferena de ser de carcter musical. +Predominantes no trabalho de Walden so guitarra e piano, mas os instrumentos que usa so relquias do incio do sculo passado, porque estes tm em seu entender mais carcter, ou, se se preferir, um som mais distintivo. +Empregou, para alm disso, tons ressonantes e profundos, de forma a aumentar a densidade emotiva. +Da resulta uma msica com uma individualidade peculiar, um misto de pureza e de permeabilidade, mas ao mesmo tempo com um sentido de passado e de amadurecimento. +O ditador ter sabido evitar, assim, as trs caractersticas que marcaram os regimes totalitrios de Hitler e Estaline: modernizao, mobilizao e expanso. +Salazar quis reinserir Portugal na sua tradio, no queria a modernizao mas a secularizao, disse Braga de a Cruz. +Tambm no tinha veleidades de mobilizao, pois o seu grande intuito foi despolitizar, baixar a febre poltica. +Ele herdou um imprio colonial e o seu objectivo foi nacionaliz-lo, conserv-lo. +No h nada nela que obrigue o regime a ser ditatorial. +Nem sequer havia proibio de partidos. +Como que, ento, aconteceu a ditadura? +Pela via administrativa. +Atravs da policializao do Estado e do esvaziamento do sistema representativo. +Com o tempo, o poder legislativo foi transferido para o Governo, esvaziando os poderes da Assembleia Nacional, que funcionou apenas trs vezes por ano, disse Braga da Cruz, que, apesar disso, encontra algum pluralismo no salazarismo. +A Unio Nacional era uma grande frente onde Salazar tolerava algum pluralismo orgnico, nomeadamente a Causa Monrquica, que, em 1961, se no fosse o incio da guerra em frica, teria mesmo aparecido como alternativa Unio Nacional. +Kadhafi, da Lbia, far o mesmo. +A Bolsa cai e o preo do petrleo sobe. +As pessoas que assistem a tais sesses saiem loucas, histricas, falam alto, as crianas choram, observa a administrao daquele condomnio. +Todas estas prticas deveriam ter locais especficos. +Segundo se julga, o cinema foi licenciado para exibio de filmes e no para culto, defende. +A finalizar, o protesto solicita uma interveno do presidente da Junta de Freguesia, a fim de esclarecer esta situao ou, se possvel, acabar com as sesses. +O autarca remeteu cpias do ofcio delegao de sade responsvel pela rea de Alverca e administrao da Lusomundo, empresa proprietria da sala de cinema em causa. +Afirmaes extraordinrias exigem provas extraordinrias. +Drosnin e muitos dos seus seguidores aceitam acriticamente o episdio Rabin como prova extraordinria do cdigo da Bblia. +Mais grave, a afirmao dos media de que foi demonstrada cientificamente a realidade dos cdigos. +no s as tcnicas utilizadas no demonstram nada, como as concluses se baseiam em falcias, como todo o processo nada tem a ver com Cincia, ao contrrio do que as mquinas de propaganda pretendem fazer crer. +A tcnica utilizada por Drosnin muito simples. +Na descrio que se segue realizo algumas simplificaes, bem como uma adaptao do hebraico para o portugus. +Passo 1: pegue num texto qualquer, de preferncia grande. +Passo 2: elimine todas as vogais, espaos e pontuao, ficando apenas com uma longa cadeia de consoantes. +Passo 3: pense num nmero inteiro, digamos 7. +Passo 4: faa uma bola em torno de uma consoante qualquer da sua cadeia, e a partir dessa em torno de todas as consoantes contadas de 7 em 7 (7, 14, etc.), construindo uma nova cadeia de consoantes. +Passo 5: pegue na nova cadeia e tente construir uma ou vrias palavras com sentido introduzindo vogais onde quiser. +Se sim, BINGO! +Acertou. +Se no, repita os passos 3 a 5 variando o que quiser at conseguir alguma coisa interessante. +Pode, por exemplo, comear a nova cadeia em qualquer das milhares de consoantes sua escolha. +Ou, em vez de contar de 7 em 7, pode contar de 33 em 33. +Ou acrescentar vogais diferentes. +Tem milhares de milhes de variaes ao seu dispor. +o nome de Rabin, na previso de Drosnin, aparece contando cada 4772 letras a partir da 4333. +A abertura da nova Ponte Vasco da Gama parece estar a influenciar de uma forma positiva o trnsito na cidade de Vila Franca de Xira. +A Polcia de Segurana Pblica vila-franquense efectuou, durante o ms de Abril, um conjunto de recolhas de dados sobre a circulao na cidade e detectou uma reduo de trfego da ordem dos 20 por cento comparativamente com as mdias registadas em 97. +A moo apresentada pela bancada socialista defendeu com rigor o projecto do Governo para a criao de regies administrativas, considerando-o uma boa base de trabalho, e acrescentou-lhe argumentos para impor a necessidade da sua concretizao. +Orlando Magalhes recorreu a dados estatsticos para provar que o Norte do pas, apesar do seu peso econmico, tem sido a regio menos beneficiada pelos fundos comunitrios, em relao s zonas do Sul e, por isso, menos desenvolvida. +As assimetrias regionais foram, de resto, os argumentos base para defender uma moo que considera a regionalizao uma reforma essencial ao Estado. +A bancada da CDU concordou e reforou politicamente o teor do documento socialista. +A regionalizao, alm de promover o desenvolvimento, favorece a democracia participativa atravs do voto popular, disse o comunista Antnio Graa. +Eu nasci e vivi em Vila Real at aos 17 anos e por isso senti na carne o isolamento do interior. +No entanto, o deputado popular acusou receios de bairrismos exacerbados e acabou por seguir a linha do lder do partido, ao rejeitar a criao de regies em favor de mais poder para as autarquias. +O presidente da Cmara de Esposende, Alberto Figueiredo, anunciou ontem, que vai pedir ao ministro das Finanas um inqurito a todos os fundos comunitrios que as suas empresas receberam nos ltimos anos. +O pedido, feito durante uma conferncia de imprensa, surge na sequncia das insinuaes que o candidato do PP, Franklim Torres, lhe fez de ter utilizado o seu cargo de presidente da autarquia para obter este tipo de apoios do Governo. +Figueiredo, que se recandidata a um terceiro mandato, considera que o seu adversrio est a insinuar corrupo e que a sua dignidade foi posta em causa, por isso no tem outro caminho seno esclarecer estes processos para que no fiquem dvidas. +Alguns estudos em que foram usadas definies de depresso mais restritas do que as internacionais apuraram taxas mais baixas -- de trs a cinco por cento. +No entanto, este especialista, psiquiatra no Hospital de Santa Maria, considera que no h razes mdicas, culturais ou sociais que nos permitam pensar que a frequncia da depresso seja diferente no nosso pas, pelo que os dados internacionas podem ser extrapolados com alguma segurana. +As principais preocupaes mdicas da actualidade so, segundo Filipe Arriaga, a elevada morbilidade associada depresso e ao suicdio. +A depresso o principal factor de risco do suicdio, est em primeiro lugar nas causas que levam ao acto suicida, acrescenta. +O PRIMEIRO-MINISTRO israelita, Yitzhak Rabin, e o chefe da Autoridade palestiniana, Yasser Arafat, conferenciam amanh em Erez, ponto de passagem entre a Faixa de Gaza autnoma e o Estado de Israel, afirmou ontem agncia noticiosa France Presse um conselheiro do chefe da OLP. +A pea do jovem dramaturgo Sergi Belbel (Prmio Nacional da Literatura Catal em 91, 92 e 93) tem muito de futurista. +Mas o futuro de que fala mais ou menos prximo. +Represso, discriminao, transgresso so as molas da comdia posta em cena por Ins Cmara Pestana. +O repressor apela para o transgressor e vice-versa. +A represso do tabagismo, por exemplo, atrai a transgresso. +E dessa transgresso que nasce o espectculo cruel do Teatro do Sculo. +Uma verdadeira dana da morte. +Ou da vida tomada como um desporto de altssima competio. + proibido fumar. +Mas toda a gente fuma (s escondidas) incluindo os proibicionistas. +Se, espectador distrado, os nomes de actores como Teresa Roby, Ins Cmara Pestana, Antnio Filipe, Elisabete Piecho, Manuela Pedroso, Marina Albuquerque, Orlando Sgio e Rui David no te dizem nada, aproveita a noite de hoje (s 22h) para ir Rua do Sculo ver o prodgio de uma companhia em que os actores parecem ligados corrente. +Porque corrente magntica no falta encenadora, que, em espectculos como Crimes e Interldios, Carcias, Kvetch e Zucco, fez do Teatro do Sculo uma referncia incontornvel no panorama do teatro portugus nos anos 90. +O SECRETRIO-GERAL da Organizao das Naes Unidas (ONU), Butros Butros-Ghali, deixou ontem ao princpio da tarde Luanda com a promessa de que at ao fim de Agosto devero chegar a Angola as unidades de infantaria que faltam para se completar a Misso de Verificao (Unavem III). +Ele pintor, 26 anos, chamado Johnson, ela professora, 28 anos, Edel de seu nome, entraram no carro de um taxista de apelido So Pedro, 40 anos. +A troca de palavras menos agradveis comeou logo, antes de arrancarem, por causa da curta distncia da viagem. +No sinal vermelho seguinte, a coisa ficou to feia que o casal abriu as portas e saiu do carro. +O taxista no se ficou e, com a segurana que a dimenso do seu porte lhe d, foi atrs deles, exigindo o seu dinheiro. +A actual administrao da TVI, presidida por Miguel Paes do Amaral, lder do grupo SOCI, e a Antena 3 espanhola assinaram ontem um acordo de parceria estratgica, em que se comprometem a duplicar o actual capital social do quarto canal portugus. +A Media Capital (do grupo SOCI), que gere a TVI, apresenta assim mais um argumento para a assembleia de credores que se realiza no prximo dia 14. +Nessa assembleia devem ser apresentados, pelo menos, dois projectos de recuperao da TVI: o da Media Capital e da Antena 3 (a principal televiso privada espanhola) e o do grupo Lusomundo, associado ao empresrio macaense Stanley Ho. +De momento, ambos os grupos esto em contactos com os vrios credores para assegurarem o respectivo apoio, pois necessrio que os votos favorveis a uma das propostas venham de um conjunto de credores que detenham pelo menos 75 por cento da dvida da TVI, que ascende a 17 milhes de contos. +Entre as mais belas fotografias de cinema do mundo as da agncia Magnum ocupam o primeiro plano. +so as que procuras os actores, mas tambm os maridos e mulheres e amigos; as cenas de repouso, o cinema no trabalho. +Estas imagensque publicamos de Cartier-Bresson, Robert Capa, Eve Arnold ou Dennis Stock, entre outros, demonstra, uma vez mais, a vocao totalizadora da Magnum, a capacidade em acompanhar de perto fenmenos mais importantes da histria da humanidade. +Uma exposio de fotografias, concebida em conjunto com o livro Magnum-Cinema, poder ser vista em Fevereiro, em Lisboa, na Culturgest. +Ter como amigos pessoas sbias, bem sucedidas e eventualmente belas um privilgio. +Digamos que a qualidade dos amigos, para alm da qualidade da sua amizade, constitui para mim um factor importante de qualidade de vida. +Que o meu querido amigo Joo Carlos Espada tenha acesso aos sabores duma das tradicionais bolsas da erudio ocidental constitui um contributo, no despiciendo, para a melhoria da minha qualidade de vida. +Mais tarde ou mais cedo, quando nos encontrarmos, usufruirei, eu tambm, das experincias de que ele agora desfruta. +Entretanto, eu -- e os milhares de pessoas que o leram no PBLICO (da passada segunda-feira) -- vou-me contentando com as suas Cartas de Inglaterra. +Um quarteto formado por Bob Mover (sax alto e voz), Carlo Morena (piano), Pedro Gonalves (contrabaixo) e Joo Silvestre (bateria) actua a partir das 23h, na catedral lisboeta do jazz: no Hot Clube de Portugal. +Uma autntica big band, constituda por 16 msicos, com alguns dos temas mais conhecidos do reportrio standard liderada pelo trombonista Claus Nymark e com a voz de Ana Paula Oliveira: no Speakeasy (Cais da Rocha Conde d'bidos -- Armazm 115), s 23h e 01h. + Alguns milhares de trabalhadores afectos CGTP desfilaram ontem pela baixa de Lisboa em protesto contra a poltica econmica e social do Governo. +Um boneco cabeudo baptizado de Santo Cavaco foi a estrela da manifestao, que partiu dos Restauradores e foi at Praa da Ribeira, depois de interromper o trnsito das Ruas do Ouro e da Prata. +No podemos permitir que a contratao colectiva continue bloqueada, que o desemprego continue a aumentar, que a segurana social, a sade e a educao continuem a degradar-se. +Somos obrigados a tornar os nossos programas o mais interessantes possvel, num ambiente onde existem mltiplas escolhas, refere Victor Neufeld, o produtor executivo do programa 20/20, da ABC, citado pela Associated Press. +Conseguir um bom exclusivo pode significar a entrada de milhes de dlares em publicidade. +Da que Shapiro tenha ficado to orgulhoso com o exclusivo dos pais dos sete irmos gmeos. +E, para que fique registado, o produtor afirma que a NBC no pagou ao casal McCaughey. +Shapiro acredita que eles concordaram em aparecer no programa Dateline, da NBC, pura e simplesmente porque gostam de o ver. +Quanto presena dos seguranas porta de casa deles, foi a NBC que os contratou para manterem os paparazzi distncia durante a entrevista. +Seriam 9h30 quando os trs homens, de quem no se conhecem dados identificativos, surgiram de armas na mo e rosto coberto porta do BNU de Massam, uma dependncia recentemente inaugurada e localizada na denominada Sexta Fase da Urbanizao Pimenta e Rendeiro (rea que na sua maioria ainda est em construo). +Os assaltantes atraram a ateno de uma das funcionrias, que deu de imediato o alarme, pelo que o gerente conseguiu bloquear a porta de entrada ainda antes desta poder ser aberta pelos suspeitos. +Aps verificarem que a entrada havia sido bloqueada, os trs homens fugiram de imediato para um automvel Citren AX, de cor branca, cujas letras da matrcula so AJ, e tomaram a estrada que conduz a Queluz. +Segundo o comandante da GNR do Cacm, entidade a quem primeiro foi comunicada a ocorrncia, no foi consumado qualquer acto violento. +Quanto a saber donde que vem a nossa preferncia quase unnime pela mo direita, a questo permanece em aberto. +Tanto mais quanto, hoje em dia, o hemisfrio esquerdo (dos dextrmanos) j no considerado como o hemisfrio cerebral dominante, nem como o nico capaz de desempenhar altas funes mentais e de controlar movimentos precisos e subtis. +Sabe-se que o hemisfrio direito tambm essencial para funes mentais de altssimo nvel, tais como o raciocnio, a memria, o reconhecimento dos rostos, das melodias musicais, etc. +Uma explicao possvel da hegemonia dextrmana poder ser o facto de, nos dextrmanos, o hemisfrio cerebral esquerdo possuir ligaes nervosas sensoriais e motoras para os dois lados do corpo, enquanto o hemisfrio direito est quase s ligado a um nico lado do corpo. +Isto poder significar, simplesmente, que o controlo voluntrio da mo direita mais fcil do que o da mo esquerda. +As delegaes do Governo de Luanda e da Unita rubricam o protocolo das conversaes de Lusaca para que haja paz em Angola. +O acordo definitivo dever ser assinado dentro de quinze dias. +Marques Mendes, ministro-adjunto da presidncia de Conselho de Ministros com a tutela da comunicao social, ameaa proceder a alteraes legislativas caso as televises no cheguem a acordo sobre a passagem de imagens de alegada violncia. +O provedor recomenda, assim, a imediata transferncia do agente da PSP para um estabelecimento prisional tutelado pelo Ministrio da Justia e que permita compatibilizar as exigncias de segurana com o exerccio dos direitos conferidos a qualquer recluso em ambiente prisional normal. +A falta de conscincia do sentido de ridculo ainda recentemente foi dada, em declaraes ao PBLICO, por um douto professor universitrio que classificou liminarmente de mau plano um PDM elaborado dentro de uma cmara municipal com uma equipa tcnica de qualidade e fortemente assessoriada por urbanistas experientes. +Fazer uma gesto urbanstica inteligente, dialogante, eficaz, informada e com bons resultados prticos, exige uma tenacidade e um talento que no esto ao alcance de tcnicos cinzentos e submissos que o sistema inevitavelmente prefere, produz e atrai. +O incndio que deflagrou no final da semana passada nos concelhos de Mao, Abrantes e Sardoal, foi dado como extinto pelas 12h00 de ontem. +O jogo dos campees escoceses, apesar das mltiplas tentativas dos futebolistas do Rangers de maior nome, como Hateley e Durrant, no s perdia mpeto mas morria em qualidade, quando enfrentava o meio-campo do Celtic, onde McStay se exibia a grande altura. +Por outro lado, os defesas-centrais do Celtic, Mowbray e Grant, sempre coadjuvados por McGinlay, chegaram para reduzir a pouco o ataque do Rangers, onde Hateley no dispunha do apoio habitual. +O primeiro tempo terminou sem golos. +Na segunda parte, apesar de o Rangers ter tentado o tudo por tudo para rapidamente resolver a partida, o Celtic depressa recuperou o comando das operaes. +Mas o jogador russo do Rangers Mikhailichenko comeou a evidenciar-se devido frescura e rapidez do seu jogo. +Era de facto por ali que comeava a surgir perigo para o Celtic, e no surpreendeu que, com pouco mais de uma hora de jogo, fosse esse mesmo Mikhailichenko a colocar a bola frente de McCoist, de modo a permitir-lhe a marcao do primeiro golo da partida. +Cerca de trs centenas de trabalhadores municipais manifestaram-se ontem diante da Cmara Municipal de Sintra, exigindo da presidente socialista Edite Estrela o pagamento das horas extraordinrias e das ajudas de custo e a reposio do subsdio de insalubridade, penosidade e risco. +No fim, saram com algumas promessas e um nim para o subsdio. +Face a o marco, a nota verde no apresentou uma tendncia definida, oscilando entre os 1,79 e os 1,80 marcos por dlar, enquanto no mbito do SME se mantinha a tradicional apatia. +Em o fecho dos mercados europeus,o marco/escudo transaccionava-se a 102,35/ 102,36 escudos por marco,tal como tinha j acontecido tera e quarta-feira. +Em o que respeita s taxas de juro,o Bundesbank confirmou a expectativa do mercado ao anunciar uma manuteno de todas as suas taxas directoras. +Os bancos centrais francs,holands,austraco e suo tomaram idnticas decises. +ALVO: Com este afluxo -- recorde de votao, a A.B.P. saiu vencedora? +E.G.: A A.B.P.ganhou bastante e acrescento que nunca houve em Delegao nenhuma,uma to grande afluncia de scios para a votao,como se verificou nestas Eleies. +Isto significa que os Barmens no esto esquecidos. +Continuo a dizer que acho bem que os Barmens saibam que a Delegao est aberta para todos eles,e gostaria imenso que eles aparecessem em vez de uma vez por ms, dez vezes porque temos todas as condies criadas para os receber. +Podemos beber um copo,jogar gamo,damas,snooker,ver televiso,etc. +Judo (52.500 contos)Nelson Brs,Slvia Henriques,Pedro Caravana,Michel Almeida,Guilherme Bentes,Pedro Soares e Filipa Cavalleri. +Lutas Amadoras (7 mil contos)-- David Maia. +um documento de meados do sculo XVII refere j uma Rua de o Reimo. +da que surja muitas vezes referida como Campo do Cirne. +Rua onde ainda hoje se concentram algumas das mais antigas casas de mrmores e cantaria da cidade,a Avenida de Rodrigues de Freitas conta ainda com duas instituies de especial relevo para a histria da cidade,descontado o j referido recolhimento para rfs: a Biblioteca Pblica Municipal do Porto -- cuja fachada lateral est voltada para a Rua de D.Joo IV e para o Jardim de Marques de Oliveira (vulgo de S.Lzaro)-- e a Escola Superior de Belas-artes do Porto. +Passados seis meses sobre o anncio oficial da existncia da arte paleoltica do Ca,continuamos pois espera de iniciativas vlidas e de boa f para que,independentemente da deciso final que venha a ser tomada sobre a barragem,o monumento seja estudado com os meios que a sua grandeza requer. +At quando permitiro os cidados portugueses,cujos impostos sustentam a hierarquia do IPPAR,e cujas contas de electricidade alimentam os oramentos sumpturios da EDP,que esta vergonha continue? +O segundo dia ter a economia como tema comum s trs sesses. +Incluir,naturalmente,o debate sobre a UEM,numa perspectiva europeia e portuguesa,a posio europeia na economia mundial e,ainda,o papel da UE no processo de reintegrao da Rssia na economia internacional ou a dimenso econmica e poltica das relaes transatlnticas. +Entre os participantes,estaro Alan Milward,Jacques Attali,Vtor Constncio,Leonardo Ferraz de Carvalho,Vtor Martins,Artur Santos Silva,Jorge Jardim Gonalves e Pedro Ferraz da Costa. +Finalmente,o terceiro dia ser dedicado a uma reflexo mais profunda sobre essa entidade a que se chama Europa -- do ponto de vista da sua cultura e da sua histria. +Para responder questo primordial -- o que a Europa?-- intervm na conferncia personalidades como Alain Finkielkraut,Edgar Morin,Susan Sontag ou Marcelo Rebelo de Sousa. +Andr Glucksman,Peter Schneider e Victor Cunha Rego falaro de cultura europeia,e o americano Simon Shama abordar a procura de uma histria europeia. +Em os mintos iniciais da segunda parte os vitorianos conseguiram de novo adormecer os seus adversrios,nomeadamente graas a o trabalho laborioso de Paneira e Tonio e a excelente marcao que Marco exerceu sobre Zola. +O portugus secou de tal forma o gnio parmeso que Ancelotti acabou por substitui-lo por Melli. +O Parma voltou a acordar aos 66',quando Melli rematou barra uma bola passada por Dino Baggio. +Porm,quando parecia que os italianos iam embalar para uma exibio arrasadora,aconteceu o golpe de teatro. +Acabadinho de entrar a substituir Riva,o brasileiro Gilmar surgiu isolado na rea italiana e deu o melhor seguimento a um centro de Quim Berto. +Os adeptos italianos gelaram,enquanto a pequena comitiva vitoriana festejava ruidosamente na bancada central. +de novo a polmica ribalta. +Um investimento de 27 milhes de contos (mais seis milhes para aquisio de uma coleco prpria)suportado por as arcas bascas apesar de a existncia de 47 partenaires comerciais, sinnimo do interesse que o governo de Euskadi,do Partido Nacionalista Basco (PNV),atribui ao evento. +Implantado em terrenos antes ocupados por indstrias desarticuladas por a reconverso industrial dos anos 80 e cujo ferro velho permanecia como legado da crise econmica da outrora laboriosa cidade de Bilbau,o Museu pretende ser um smbolo da regenerao basca: de uma sociedade que quer vencer as dificuldades presentes e,em vsperas do sculo XXI,dar novos argumentos -- os da esperana e da paz -- aos seus cidados. +Por isso,atentos ao perder da sua influncia,os etarras,atravs de o at agora desconhecido comando Katu,tentaram destruir o smbolo de um novo tempo,menos rural -- nessas zonas e entre os deserdados das cidades que a Eta faz o seu recrutamento --,mais dinmico e,sobretudo,aberto influncia exterior,algo que o nacionalismo admite com dificuldade. +Desvalorizando,por outro lado,a posio assumida por o deputado independente Jos Magalhes,que considera errada a proposta de Manuel Carrilho -- Se ele disse isso,o problema de ele,foram as suas palavras --,Jorge Coelho reafirmou a sua confiana no ministro da Cultura. +Se ele apresentou esta soluo para resolver aquelas dvidas, porque deve ser uma boa soluo,rematou. +poca de Ouro abre, com chave d'ouro, o ciclo. +Fundado por o mestre Jacob do Bandolim em 1964,o grupo tem mantido viva,ao longo de os anos,a tradio no domnio do chorinho,valorizando-a com sabedoria. +Compem o grupo seis msicos de reconhecida craveira: Toni (violo),Csar Faria (violino),Jorge Filho (cavaquinho),Ronaldo do Bandolim (bandolim),Cristvo Bastos (piano)e Jorginho do Pandeiro (flauta). +Hoje e sbado,s 21h30,uma oportunidade nica para confirmar,em palco,toda a alma e virtuosismo de um gnero musical tambm nico. +Com o apoio da Fundao luso-brasileira para o Desenvolvimento da Lngua Portuguesa. +s 17 horas,pode ser visto Adamubies (ttulo de um poema de Guimares Rosa),projecto de msica cnica onde textos de Miguel Torga,Clarice Lispector,Joo Guimares Rosa e Agustina Bessa-Lus surgiro vestidos com msicas reinventadas a partir de matrizes populares do Brasil,frica e Portugal. +A direco musical de Tilike Coelho e a encenao e cenografia de Jos Caldas Neto. +A estes juntaro-se-,em palco,os cantores/msicos Marta Silva,Marcelo Lafontana e Pedro Ribeiro. +O espectculo repete domingo mesma hora e no fim-de-semana seguinte,11 e 12,tambm s 17h. +O inter-Fiorentina era o jogo grande da terceira jornada do campeonato italiano. +A partida at comeou bem para os homens da casa. +Ronaldo, beira de o intervalo,inaugurou o marcador para o Inter,justificando o forte investimento que a equipa milanesa fez na sua aquisio. +Mas os homens de Gigi Simone tiveram que sofrer muito para levar de vencida a Fiorentina que,com golos de Serena e Batistuta,deu a volta ao resultado. +Batistuta viu ainda dois remates seus baterem nos postes da baliza contrria. +Um golo de Moriero recolocou as duas formaes em p de igualdade e s a nove minutos do fim o Inter chegou ao golo da vitria,por intermdio do francs Djorkaeff. +Em os outros jogos do calcio,destaque para mais uma goleada da Juventus de Dimas (4-0 frente a o Brescia),enquanto a Lazio de Eriksson parece ter perdido a veia goleadora demonstrada em Guimares e foi derrotada por o recm-promovido Empoli,por 1-0. +Realce tambm para mais uma derrota do Milan,desta feita por 2-1,frente a a Udinese. +Agora, perante os olhos da velha Elvira, sentada no centro da sala com os xailes e o casaco que a protegem do frio, encontra-se o vestido verde com os botes com pequenos sabres dourados idealizado pela costureira Danilina. +Rev-se no Mercedes de Goebbels, apreendido na Alemanha e entregue ao marechal Oslikovsky, sentado com a filha pequena, Lora, no assento posterior. +Uma corrida para Moscovo a chupar as bolinhas de Kliukva v sacarnoi pudre, feitas de acar em p, com uma baga siberiana de sabor agridoce no meio. +Nessa manh, usava pela primeira vez os sapatos de pele macia criados por Goldin, o surdo sapateiro judeu. +Tinha calado um par de pantufas inchadas de plo velho. +De sbito, ergue os olhos para voltar a mergulhar no passado que ressumava daqueles trajes velhos. +Eis o casaco de linho claro e a saia que lhe havia dado a filha casada em Itlia. +Uma medida grande para o seu corpo pesado, agora que tinha setenta e dois anos. +Usou-o no navio de cruzeiro Taras Scevcenko, que partia de Odessa. +Estava calor e ela enxugava o suor com um leno bordado apertado entre os dedos brancos e resplandecentes dos anis, ainda que agora de pouco valor. +Quando o navio estava a passar no estreito dos Dardanelos e as cpulas das mesquitas de Constantinopla pareciam pombas brancas numa atmosfera de palhetas douradas, apresenta-se-lhe uma velha senhora magra, elegante e pripudrennaia (empoada) que a submerge numa admirao comovida, quase como se se tratasse de uma irm ou de um familiar regressado do tmulo. +Esta senhora senta-se junto dela e, enquanto lhe acaricia as mos e procura lev-las at s faces magras, comea a recordar-lhe o tempo passado, a sua beleza extraordinria, que se tornara uma lenda em toda a Rssia e nas repblicas mais quentes. +Recordou-lhe os invernos em Bakuriani, quando percorria os caminhos da aldeia a comer tangerinas e a macular, com as cascas, os montes de neve branca ao longo das estradas, por entre as casas de madeira. +E os admiradores seguiam-na de longe, recolhendo aquelas cascas para ficarem qualquer coisa dela. +Recordou-lhe quando, no fim da guerra, passava devagar na Avenida Gorki com o carro que pertencera a Goebbels. +Falou da praia de Soci e do Castelo d'Aria, no cimo da colina de Dorzacos, quando o chefe da orquestra bebia copos de gua gaseificada, apontou o pequeno restaurante do Ermitage em Moscovo, onde se comiam trutas. +Os beijos que todos procuravam dar-lhe na Praa Vermelha no dia da vitria. +O seu vestido escuro durante o funeral de Estaline, quando, apesar da multido que se apinhava ao longo dos muros, para ela havia espao, para que ningum sequer lhe tocasse. +Depois, os longos anos de ausncia dos teatros, dos restaurantes e das praias da moda. +Um aviso fugaz no grande mercado das aves, quando a indiferena da multido lhe permitia observar com tranquilidade um peixinho circunscrito gua de uma garrafa e j ningum era atrado pelo seu perfume, que ainda ento se evaporava da carne rosada e abundante. + Finalmente, eis que podia rev-la e abra-la. +Subitamente, a mulher empoada recolhe todo o seu afecto nos olhos claros e avermelhados pela comoo. +Mas a senhora lembra-se de quando era nova? -- pergunta-lhe de sbito. +Nesta onda de apertada concorrncia em que os bancos tm vivido nos ltimos anos, parece que no so apenas os clientes que por vezes ficam baralhados com tantos produtos novos, taxas de juro irrecusveis e remuneraes estonteantes. +O turbilho de tal forma irresistvel que at as prprias instituies ficam abananadas e entram, olimpicamente, pela via do disparate a toda a prova. +Vem isto a propsito da recente experincia do depositante de um conceituado banco. +Segundo o relato do prprio, houve um dia em que foi caixa do correio e l encontrou o familiar envelope que regularmente o informa acerca do movimento da sua conta bancria. +Parecia um pouco volumoso para to pouca coisa, mas logo se colocou a hiptese de o extracto ir acompanhado de algum folheto de publicidade a um novo produto verdadeiramente arrasador. +Aberto o envelope, a surpresa no podia ser maior. +L dentro, alm do que era suposto l estar, havia mais treze papelinhos. +Nada menos que os extractos de conta de outros tantos clientes que a esta hora devero estranhar o facto de o seu banco no lhes passar carto acerca dos seus saldos. +se tinha sido nomeado gestor de conta de toda aquela gente ou se era o banco que estava a desenvolver alguma aco de luta contra o sigilo bancrio ... +A eurodeputada portuguesa Maria Santos uma das personalidades convidadas pela Confederao dos Sindicatos Agrcolas Bascos para as jornadas Agricultura Viva para o Meio Rural, que decorrem em Bilbau. +Em debate esto a relao entre os problemas dos agricultores europeus e as questes relacionadas com a defesa do meio ambiente, o desenvolvimento regional e os direitos dos consumidores. +em Barcelos, o quei recebe o Benfica, partida que poder servir aos minhotos para recuperarem de uma campanha fraca (trs derrotas e um empate), enquanto os benfiquistas tentaro manter o percurso vitorioso. +Na outra partida, a Oliveirense joga em casa com o sensacional Pao d'Arcos, actualmente em quarto lugar, a trs pontos do lder. +O FC Porto no ter especiais dificuldades, pois joga nas Antas com o HC Sintra. + primeira vista, Mousa, Mohammed, Abu Wahed e Khalid, o cabo, tinham a expresso deprimida e encurralada dos recrutas derrotados. +Os uniformes estavam amarrotados e sujos, as botas imundas e rotas. +Tinham a barba por fazer, estavam exaustos e famintos. +Em o saco havia nacos de po, uma lata pequena de sumo e uma garrafa de gua. +Em Frana, a jornada deste fim-de-semana provocou grandes alteraes no posicionamento dos primeiros classificados, com vantagem para o Auxerre, que chegou ao primeiro lugar, e para o Nantes, que, apesar de derrotado, beneficiou dos maus resultados do Paris Saint-Germain e do Mnaco e no abandonou o comando. +Auxerre e Nantes dividem agora o primeiro lugar, ambos com 26 pontos, 35 golos marcados e 18 sofridos, seguidos pelo Mnaco, ainda com 26 pontos, pelo Marselha, com 25 (menos um jogo), e pelo Paris Saint-Germain, com 24. +A 19 jornada comeou na sexta-feira, com a vitria do Marselha no Parque dos Princpes (0-1), perante a equipa treinada por Artur Jorge. +No sbado, o Nantes foi surpreendido no terreno do Toulouse (2-0), cedendo a terceira derrota da poca, enquanto o Auxerre recebeu e venceu o Le Havre (4-1). +No campo do Lyon, o Mnaco no foi alm de um empate sem golos, o que impediu a equipa de Rui Barros de se isolar no primeiro lugar. +A prova de que a exibio de O Convento deixou muita gente baralhada podemos encontr-la, por exemplo, naquele jornalista do New York Post que fala sempre em scoops -- do estilo quem est a filmar com quem -- e que na conferncia de imprensa de apresentao do filme declarou: Sr. Oliveira, o seu filme o meu favorito de todo o festival. +Diga-se, tambm, que h quem pense o contrrio. +Mas ontem, Catherine Deneuve e John Malkovich foram, de facto, as atraces da imprensa. +Ao lado de Lus Miguel Cintra e de Leonor Silveira, apresentados como os guardies da casa de Oliveira, Deneuve e Malkovich eram os estranhos seduzidos pela obscuridade -- palavra de Deneuve, lembrando-se de Buuel. +E como disse a actriz, h certas coisas que no se devem desvendar. +Malkovich, por seu lado falou em primitive dream paintings. +Silenciosos, os guardies observavam. +Foi com eles que o PBLICO falou. +basta-nos procurar compreender o contedo do acordo agora assinado. +E tendo integrado o ncleo fundador da Plataforma -- sem estarmos sequer ligados ciso que, circunstancialmente, no PCP lhe deu origem --, naturalmente sem satisfao que vemos a Plataforma dar razo a posteriori a lvaro Cunhal. +Acreditando na possibilidade e na necessidade da construo de uma terceira fora na esquerda portuguesa, com uma lgica nova de interveno e funcionamento, dar razo a Cunhal tambm dar razo a todos os que ainda no tiveram coragem de arriscar este projecto. +O texto do Protocolo de Cooperao suficientemente vago e impreciso para ser incuo e, ao mesmo tempo, ter um significado poltico claro. +Embora reafirmem a cada dois passos o carcter estritamente autrquico do acordo, os dirigentes da PE subscrevem um texto que vai desde a reforma do sistema poltico em geral at ao modelo de desenvolvimento para o pas. +No ltimo pargrafo das seis longas pginas, conclui-se por uma viso convergente sobre a necessidade de reformar positivamente o sistema poltico no sentido de assegurar melhor democracia e melhor desenvolvimento, na concretizao de uma alternativa poltica ao PSD e ao Governo. +A 45 anos de governao do Partido Nacional, quatro dos quais sob a batuta de Frederik Willem de Klerk, dever seguir-se agora um perodo indeterminvel de preponderncia poltica do Congresso Nacional Africano (ANC), criado em 1912 para conseguir o fim do racismo. +As sondagens opinio pblica indicam que o ANC, sob a liderana de Nelson Mandela, poder conseguir de 58 a 60 por cento dos votos nas eleies de Abril para a Constituinte. +E alguns observadores vem em semelhante vantagem o perigo de uma tentao hegemnica, se bem que a Constituio interina a aprovar durante as prximas semanas preveja a representao no governo de todos os partidos que consigam pelo menos cinco por cento dos votos. +O lanamento de estgios para jovens desempregados ou procura do primeiro emprego, o reforo de incentivos j existentes ao emprego por conta de outrm ou criao do prprio posto de trabalho bem como a sistematizao de informao sobre alternativas de formao e de profisses so as trs principais medidas do Programa para a Integrao dos Jovens na Vida Activa. +menos de um ms aps ter vencido o Campeonato Nacional de Clubes em golfe, disputado no Oporto Golf Club, coloca na final do Individual dois elementos que contribuiram para a conquista desse ttulo, Stephane Castro Ferreira e Jos Sousa e Melo. +O FC Porto conquista a sua oitava Taa de Portugal ao vencer o Sporting, por 2-1, no jogo da finalssima disputado no Estdio Nacional. +No final do jogo, adeptos do Sporting lanam garras e pedras para a tribuna de honra, onde estavam Manuela Ferreira Leite, ministra da Educao, e Vtor Vasques, presidente da FPF. +Palma Incio, ex-comandante operacional da LUAR, numa entrevista ao Expresso, afirma que no reconhece, aos que contra ele se colocam, envergadura moral para o ofender e lembra que o ELP de Spnola foi a organizao mais terrorista de Portugal. +O templo, de configurao quadrangular, com cerca de 15 metros de largo, foi descoberto quando Srgio Coutinho, proprietrio de um terreno, ali quis fazer um estabelecimento de turismo rural. +Os arquelogos chamados ao local avaliaram o achado como sendo da poca de Jlio Csar, mas s agora, aps diversas investigaes das arquelogas Ana Arruda e Catarina Viegas, acompanhadas por tcnicos do Instituto Portugus do Patrimnio Arquitectnico e Arqueolgico, se chegou a um consenso pleno. +Na mesma zona em que foi encontrado o templo, a Alcova, a caminho das Portas do Sol, foram ainda descobertas cisternas romanas que esto tambm a ser objecto de escavaes e estudos arqueolgicos. +Para o autarca Jos Noras, em declaraes agncia Lusa, o achado arqueolgico permite descodificar a presena romana na velha Scalabis e explicar a importncia estratgica de Santarm no contexto da Pennsula dessa poca. +O municpio vai agora preservar o monumento e promover o seu conhecimento por arquelogos e estudantes. +Ontem, at os futebolistas brasileiros Ronaldo, Roberto Carlos e Denilson ajudaram festa (O nosso favorito era Kuerten; agora Rios), aplaudindo o vencedor de p. +O prximo adversrio ser o artista Hicham Arazi que garantiu nova presena nos quartos-de-final do Grand Slam francs. +Vindo do anonimato em 1997 graas a quatro vitrias consecutivas (a ltima das quais sobre Marcelo Rios) em Roland Garros, Arazi (47 ATP) parece disposto a fazer melhor este ano depois de ter eliminado Alberto Berasategui (cabea de srie n16), um dos tenistas em melhor forma esta poca sobre a terra batida (vitrias-derrotas: 18-4). +No tenho feito grandes resultados ultimamente, mas quando comeo a bater umas bolas aqui sinto-me logo melhor, reconheceu o marroquino aps o triunfo por 6-2, 6-4, 3-6 e 6-3. +A economia muito, mas no tudo para aferir da fora vital que sai das entranhas de um povo. +O sistema encontra-se disponvel no mercado portugus e faz parte do pacote proposto aos franchisados. +Miguns Cardoso, da Triunfo / Il Fornaio, considera que, em termos de software, uma soluo integrada e o apoio do consultor informtico a soluo mais adequada gesto de uma rede comercial deste gnero. +O Grupo apostou nesta soluo desde 1990 e as perspectivas so para a abertura de uma nova loja por ms. +Uma associao de consumidores de Coimbra exigiu de novo a extino do Instituto do Consumidor, que acusa de ter gasto em 1994 dinheiro sem proveito visvel para os consumidores. +A Deco tem uma posio oposta e o organismo visado limita-se a propor uma leitura atenta do relatrio de actividades desenvolvidas. +O Instituto do Consumidor (IC) gastou em 1994 mais de meio milho de contos sem resultados visveis, acusa a Associao Portuguesa de Direito do Consumo (APDC), que endereou uma carta ao primeiro-ministro propondo a extino, ainda antes das eleies, daquela estrutura do Ministrio do Ambiente. +Cada dia que passa torna mais evidente que o voto dos eleitores do Entre Douro e Minho vai ser decisivo para o resultado final do referendo. + para esta regio que se viram os olhos dos partidrios do sim, ansiosos por uma votao que compense a quebra anunciada pelas sondagens noutras zonas do pas. +Mas tambm neste espao que cabem as esperanas dos que querem travar o modelo das oito regies. +se o no na primeira pergunta do referendo for maioritrio na mais populosa das oito regies propostas, isso significar, quase de certeza, a certido de bito do modelo nascido do acordo entre o PS e o PCP. +Se isso vier a acontecer, a regionalizao ficar adiada por 10 ou 15 anos, reconhece Narciso Miranda, lder da Federao do Porto do PS, que acredita numa vitria esmagadora do sim no Entre Douro e Minho. +Pense-se em Kingsley Amis, Malcolm Bradbury e Albert Finney. +Dois escritores, um actor. +Pense-se no romance que o primeiro escreveu -- The Green Man --, que o segundo adaptou e o terceiro interpretou. +Pense-se num enredo mirabolante, centrado num heri desfeito, o anti-heri Maurice Allington, e na maneira como o impensvel -- o fantasma de Thomas Underhill -- o cerca. +No fim, obtm-se uma srie de televiso. +Uma boa srie de televiso: a que a RTP estreou ontem noite, na TV2, e qual nada ligou -- pouco mais do que primeiro episdio escreveu na apresentao. +Motor: Econmico, com grande elasticidade e bom nvel de potncia. +Caixa: Mais um Toledo salvo por uma caixa bem escalonada, com destaque para as trs primeiras velocidades. +-- o nico candidato que me parece capaz de promover uma mudana de paradigma na vida poltica americana. +Clinton mais novo na idade e nas ideias. +Bush endureceu na atitude de conservao a todo o preo de uma ordem histrica condenada. +A Amrica tem de preparar-se para o futuro. +Com Bush, no conseguir faz-lo. +-- George Bush. +-- Em primeiro lugar porque conhece os problemas europeus; em segundo lugar pelo papel que desempenhou na poltica mundial nos ltimos quatro anos e, em terceiro lugar, porque o CDS sempre mais ligado aos republicanos do que aos democratas. +Para Eurico de Melo s faz sentido convocar um referendo se se chegar concluso da sua necessidade para cumprir um formalismo constitucional. +No desvalorizou, porm, a convocao do Conselho de Estado por entender que no deve subsistir a menor dvida sobre os formalismos constitucionais a cumprir nem sobre a vontade poltica de adeso ao Tratado. +Eurico de Melo defende que as decises de Maastricht so de grande importncia para o pas na medida em que reflectem uma linha de mais compromisso poltico com a CE. +Segundo a Comisso de Afectados pela Barragem do Lindoso, entidade promotora da concentrao, o objectivo perseguido mantm-se: prosseguir no seu protesto contra o que entendem ser uma atitude de chantagem da EDP. +Em declaraes ao PBLICO, um dos elementos da Comiso dos Afectados manifestou esperanas na possibilidade de reatamento do dilogo interrompido a 16 de Dezembro ltimo, remetendo para os resultados de uma reunio que ir juntar no Porto o governador civil de Ourense, o presidenta da Cmara do concelho de Lovios (Ourense) e Marques Seabra, responsvel da EDP, e que est prevista para sexta-feira, embora sujeita ainda a confirmao. +A mesma fonte afirmou que os afectados pela barragem estaro dispostos a permitir que prossigam os trabalhos de remoo da igreja de Aceredo, que ir ficar submersa pela albufeira, logo que a arbitragem acordada entre as partes seja assumida em documento assinado perante um notrio. +Em vsperas de Carnaval, a Associao Portuguesa para a Defesa do Consumidor (DECO) mostra-se preocupada com a utilizao de explosivos nas brincadeiras carnavalescas, responsvel todos os anos por inmeros acidentes, sobretudo com crianas em idade escolar. +Em comunicado, a DECO considera essencial a informao dos consumidores sobre este assunto, causador tambm de rudo e perturbao da ordem pblica, especialmente nas escolas. +As bombas de Carnaval pertencem ao conjunto de explosivos tecnicamente designado como bombas de arremesso, cuja venda -- regulamentada por lei -- s pode ser feita a pessoas, com mais de 18 anos, que tenham autorizao das autoridades competentes para as comprar. +Mas de faltas de correspondncia entre a lei e a realidade dos factos est o pas cheio. +Por exemplo, lembra tambm a DECO, entre os brinquedos preferidos pelas crianas nos festejos carnavalescos encontram-se os estalinhos, considerados brinquedos ou artifcios pirotcnicos que podem rebentar por choque ou atravs de um detonador. +Encontram-se em todo o lado e, no entanto, o seu fabrico proibido por lei. +Hoje, Bush tem na agenda as cidades de Baidoa e Bali-Dogle e mais visitas a soldados, orfanatos e organizaes humanitrias. +O nico incidente registado ontem em Mogadscio ocorreu na embaixada francesa quando um somali tentou entrar nas instalaes e no obedeceu ordem de parar de um sentinela, anunciou o comando francs da operao Oryx. +O soldado disparou para o ar, mas o indivduo continuou a avanar e foi atingido mortalmente. +Lawrence Summers evitou atacar os pases do G7, mas no pde deixar de falar no Japo, sublinhando que os EUA no manipulam artificialmente as taxas de cmbio e que seu desejo e de todo o mundo que este pas volte a ter um crescimento rpido. +O Japo respondeu que concorda com as sugestes norte-americanas e que apoia uma aco concertada para travar a valorizao do iene, considerada como brutal a ambgua -- j que se pode conduzir diminuio do excedente comercial japons pode tambm pr em causa o crescimento mundial, na opinio do secretrio do Tesouro dos EUA, Lloyd Bentsen. +As multides no acorreram abertura, domingo, da exposio A Idade do Barroco em Portugal, organizada pela National Gallery de Washington e a Secretaria de Estado da Cultura portuguesa, atravs do Instituto Portugus de Museus (IPM). +Mas a exposio estar patente at 6 de Fevereiro e provvel que, nas suas visitas de rotina aos museus, grande parte dos washingtonianos venham a visit-la. +Isto maravilhoso, um verdadeiro tesouro, que no imaginei que existisse. +Mary Sue, 55 anos, entrou com a amiga na exposio, por acaso. +Na realidade, vieram ao museu por causa da exposio sobre os Pssaros da Amrica, que est nas salas ao lado. +Mas no se arrependeram. + um povo e uma poca fascinantes. +Estamos mortas por visitar Portugal. +Estas novas atitudes correspondem clebre frase de Robert Fillion . +a arte o que faz a vida parecer mais interessante que a arte. +Esta exposio nasceu, ainda no dizer de Jean de Loisy, da leitura do clebre artigo de Allan Kaprow: A herana de Jackson Pollock. +Kaprow pretende que Pollock no teve tempo de levar as suas obras at s ltimas consequncias, as quais teriam sido, no limitar o quadro tela posta no cho, mas de nele integrar o prprio cho do atelier, os objectos, os rudos da rua, em suma, a vida. +Este artigo, escrito em 1957, posiciona a arte numa nova direco, da qual a exposio tenta ser o reflexo. +Accionismo, novo realismo, happening, poesia sonora, Fluxus, performance, arte corporal, environnements, so alguns dos nomes que, segundo os perodos e os pases, foram dados a estas novas formas de arte. +O Japo quer continuar a negociar com os EUA tendo em vista resolver o diferendo comercial que ope os dois pases, apesar de a ameaa norte-americana de aplicao de sanes, caso as autoridades npnicas no abram os seus mercados pblicos num prazo de 60 dias. +O ministro das Finanas de Tquio, Masayoshi Takemura, afirmou lamentar a atitude de Washington, mas adiantou que o Japo continuar a tentar ultrapassar as diferenas entre as duas partes, em cada um dos sectores em negociao: automvel, seguros e mercados pblicos nas reas das telecomunicaes, equipamento mdico, computadores e satlites. +O ultimato para a abertura dos mercados pblicos nipnicos foi imposto por Washington no ltimo fim-de-semana, depois do fracasso das conversaes bilaterais. +Em Portugal desde 23 de Maio, Manuel Antnio viveu em Lisboa, cerca de trs meses, deslocando-se depois para o Porto. +Pelas 3h55 da passada quinta-feira foi surpreendido nas escadas interiores da 15 Esquadra da PSP, na Foz do Douro, sem ter usado a porta de entrada. +Fonte policial admite que o jovem tenha saltado o muro das traseiras das instalaes. +Na altura, Manuel Antnio alegou sede e que queria beber gua, justificando desta forma a sua entrada na esquadra atravs do muro. +Indocumentado, deu um nome trocado -- Manuel Carneiro --, disse ser moambicano e que tinha os documentos numas obras. +Esta informao no foi confirmada pelos agentes da PSP que o acompanharam a estaleiros sitos na Foz, Boavista e no Carvalhido. +As diligncias da polcia terminaram com a sua entrega ao Servio de Estrangeiros e Fronteiras (SEF). +Junto do Consulado de Moambique, o SEF apurou ser falsa a nacionalidade indicada pelo Manuel Antnio e descobriu os documentos no interior de uma pasta na posse de um tal Albertino. +As autoridades verificaram que o Manuel Antnio tinha dois passaportes: um angolano, verdadeiro; e outro portugus, falsificado. +Este ltimo documento ter sido adquirido no Centro Comercial Dallas, no Porto, por 15 contos e destinava--se a ser usado numa viagem a Frana. +Acusado de falsificao de documentos, Manuel Antnio viu a deteno confirmada por um juiz de instruo, que no atendeu ao facto de o jovem ter menos de 16 anos, em funo do passaporte angolano de que era portador. +Quatro dias mais tarde, o Ministrio Pblico detectou o erro e ordenou a transferncia do processo para o foro do Tribunal de Menores, a quem foi entregue ontem. +A Frana alcanou, em 1995, um excedente comercial recorde de 104,5 mil milhes de francos (3,1 mil milhes de contos), o que representa um acrscimo de 27 por cento em relao aos resultados de 1994. +O ministro do Comrcio Externo, Yves Galland, anunciou ainda que, no ano passado, as exportaes francesas atingiram o montante recorde de 1427 mil milhes de francos (cerca de 43 mil milhes de contos), reflectindo uma alta de 9,2 por cento relativamente ao ano anterior. +As importaes apresentaram uma progresso de 7,9 por cento, o que equivale a 108,6 mil milhes de francos (3,25 mil milhes de contos). +O ltimo saldo recorde do comrcio externo havia sido atingido em 1993, com um excedente de 87 mil milhes de francos (2,6 mil milhes de contos). +A Comisso Europeia aguarda uma notificao das autoridades italianas sobre o plano de reestruturao da companhia area Alitalia. +O principal accionista, o grupo pblico italiano Iri, adoptou na ltima quinta-feira as grandes linhas do plano, que prev uma injeco de capital na ordem dos 937 milhes de dlares (cerca de 142 milhes de contos) e o reforo da estratgia de alianas. +A Comisso Europeia dever em breve tomar uma deciso sobre a segunda parte da ajuda pblica acordada com a companhia area grega, Olympic Airways. +Espera-se ainda a aprovao, por parte da Comisso, da terceira parte do plano de ajuda companhia francesa Air France, cujo montante global de cerca de quatro milhes de dlares (cerca de 605 mil contos). +Raramente as instituies de planeamento falam claro. +Mas, s vezes, acontece. +em segundo lugar, e trata-se de uma concluso menos taxativa, de admitir que tenha diminudo a posio relativa da Regio Norte no contexto do espao scio-econmico portugus. +Desde que o ministro do Planeamento e Administrao do Territrio, Valente de Oliveira, se reuniu em Setembro e Outubro, em Vila Real e no Porto, com as cmaras do interior e do litoral da Regio Norte, que a CCRN se tem desdobrado em contactos para discutir a forma de aplicao do prximo quadro de apoio. +Deste esforo, um contra-relgio que se destina a ouvir tanto os rgos de poder local como os agentes de desenvolvimento e essa entidade genrica a que se chama sociedade civil, dever nascer a proposta nortenha para o novo Plano Regional de Desenvolvimento, espreita dos fundos acrescidos proporcionados pela aprovao em Edimburgo do Pacote Delors II. +A equipa masculina concorre Primeira Liga, no decatlo, em Helmond, na Holanda, contra as formaes da Alemanha, Bielorssia, Estnia, Rssia, Holanda, Polnia e Suia. +Sobem Superliga (para o ano) as duas primeiras equipas e descem as duas ltimas e Portugal dificilmente escapar despromoo. + que, de facto, s por puro acaso equipas como a alem e a russa esto nesta diviso. +Portugal alinha com Mrio Anibal Ramos (Benfica), que recentemente deixou o recorde nacional em 7614 pontos, F. Nuno Fernandes (FC Porto- 7381 pontos), Jos Duro (CN Rio Maior- 6614 pontos) e Lus Herdio Costa (Sporting- 6585 pontos). +Ao mesmo tempo decorre a Superliga feminina (heptatlo), envolvendo a Bielorssia, Frana, Gr-Bretanha, Alemanha, Holanda, Polnia, Rssia e Ucrnia, com as russas favoritas para manterem o trofu. +A seleco feminina portuguesa, por seu lado, compete no heptatlo da segunda liga em Dilbeek, na Blgica, com belgas e suias. +A equipa vencedora subir primeira liga, mas para essa posio favorita a Suia. +Portugal alinha com Sandra Turpin (Benfica- 5218 pontos), Snia Machado (individual- 5061 p), Mnica Sousa (GD Cavadas- 5122 pontos) e Catarina Rafael (Bairro Anjos- 4602 pontos). +A maior perspectiva da parte portuguesa a hiptese do recorde nacional ( de 5228 pontos) por Sandra Turpin. +Desejava que tivessem visto a expresso no rosto do dr. Steve, disse ao centro de controlo da misso Ken Bowersox, comandante do vaivm, escreve a agncia Reuter. + ali no poro do Discovery que o Hubble vai ser reparado no decurso de quatro passeios no espao, com seis horas de durao cada um, que sero feitos pelos astronautas Mark Lee, Steve Smith, Greg Harbaugh e Joe Tanner. +O primeiro, o mais importante desta misso, estava previsto precisamente para a noite passada. +Se tudo correu como estava previsto, fez-se a substituio de dois instrumentos de observao do Hubble -- o Goddard High Resolution Spectrometer e o Faint Object Spectrograph -- e a instalao de dois novos instrumentos destinados a ampliar significativamente as suas capacidades de observao: o Space Telescope Imaging Spectrograph (STIS) e o Near Infrared Camera and Multi-Object Spectrometer (NICMOS), cada um do tamanho de uma cabine telefnica. +A perspectiva de as duas empresas competirem entre si sem se terem em conta os incmodos resultantes para os aveirenses acabou por ditar uma soluo de consenso, tendo a autarquia funcionado como intermediria para o bom entendimento entre as partes. +E a possibilidade de as ruas de Aveiro se transformarem num estaleiro permanente, com um operador a abrir valas onde o seu concorrente as tinha fechado uns dias antes, no seria de todo inadmissvel, at porque a legislao permite a livre concorrncia entre os concessionrios da TV por cabo. +Segundo o acordo estabelecido, a TV Cabo Mondego, que entrou j em fora no mercado local de assinantes, utiliza as fibras pticas da rede telefnica j instaladas pela Portugal Telecom, enquanto a Pluricanal se servir, mediante uma comparticipao financeira, das condutas do gs natural. +Para o presidente da autarquia, Celso Santos, este acordo, que ser formalizado atravs de um protocolo alargado EDP e Lusitniags, permitiu Cmara controlar o processo de instalao das novas tubagens e evitou que as ruas permanecessem intransitveis durante muito tempo. +O vice-presidente Andriessen participa na Reunio de Coordenao da Ajuda Internacional CEI, no Centro Cultural de Belm. +A reunio termina amanh. +Reunio informal dos ministros da Agricultura, na Curia. +Termina no dia 25 deste ms. +Ora existem muitas funes (ordenamento territorial, incentivos ao desenvolvimento, ambiente, turismo, cultura, vias de comunicao, educao, etc.), para as quais os municpios so demasiado pequenos e o Estado demasiado distante. + para isso que em todos os pases, grandes ou pequenos, existe uma autarquia territorial (ou mesmo duas) entre o Estado e os municpios. +Ns prprios, desde a revoluo liberal at 1974, sempre tivemos acima do municpio ora o distrito, ora a provncia. +As regies administrativas no so mais do que a restaurao da figura das provncias, com atribuies mais centradas no ordenamento territorial e no desenvolvimento. + evidente que as regies administrativas tero tambm condies para reivindicar uma mais equilibrada repartio dos recursos oramentais, mesmo nas funes que ho-de continuar a ser do foro da administrao central. +Nas minhas deslocaes de comboio a Lisboa no posso evitar um quase sentimento de revolta, quando comparo a escandalosa indigncia da estao de caminhos-de-ferro de Coimbra, mais prpria de um apeadeiro terceiro-mundista, com a sumptuosidade megalmana da nova Gare do Oriente, que pelos vistos corre o risco de vir a ser o mais oneroso dos apeadeiros de luxo do mundo. +E sou levado a pensar que a existncia de regies poderia contribuir tambm para evitar estas gritantes disparidades de tratamento regional ... +Que a posio do Vaticano possa ser entendida deste modo por um intelectual desta craveira -- sobretudo a ideia de um malvado voluntarismo de Deus que lhe est subjacente -- deveria fazer reflectir os argumentadores oficiais da doutrina da Igreja. +Parece-me que nela se continuam a misturar alhos com bugalhos e a no hierarquizar adequadamente nem as convices de f nem as razes. +Neste sentido, espero que o interessante documento de trabalho do Conselho Pontifcio da Famlia, Evolues demogrficas: dimenses ticas e pastorais, Lisboa, 1994) leve uma grande volta. +Se, como nele se diz, a Igreja deseja encetar um dilogo construtivo com os que continuam convencidos da necessidade de realizar um controle imperativo da populao e com os governos e as instituies que se ocupam de polticas da populao, j que existem problemas demogrficos reais, apesar de frequentemente serem vistos a partir de uma perspectiva errada e de se proporem solues depravadas para os resolver (n24), no pode favorecer os mal-entendidos. +Esta uma posio fundamental que no pode ser trocada por nada, sejam quais forem as chantagens dos patres deste mundo na Conferncia do Cairo ou fora dela. +Numa reunio com comerciantes e moradores da vila velha, realizada tera-feira tarde, os vereadores da CDU refutaram as acusaes feitas pela presidente do municpio de estarem a atrasar obras importantes, garantindo no estar contra ou a favor da construo do silo na Volta do Duche -- que se encontra em banho-maria --, mas fizeram depender uma posio favorvel da realizao de um estudo mais vasto para toda a vila que prove a necessidade daquele investimento. +Todos os partidos polticos com assento na Assembleia Municipal de Leiria contestam o Plano Estratgico do Eixo de Leiria-Marinha Grande e do Sistema Urbano da Alta Estremadura. +Contactados pelo PBLICO, os lderes das concelhias do PP, do PSD- com a maioria da Assembleia Municipal-, do PS e o deputado municipal da CDU so unnimes em considerar que o documento mandado elaborar pela Associao de Municpios da Alta Estremadura (AMAE) carece de um importanle debate pblico, havendo mesmo quem no Ihe reconhea o estatuto de plano estratgico. +Apesar disso, as diferentes foras partidrias esto dispostas, na prxima reunio da Assembleia Municipal, sexta-feira, a criarem as condies para que sejam aprovados os projectos a candidatar pela Cmara de Leiria e pela associao de municpios ao Prosiurb (Programa de Consolidao do Sistema Urbano Nacional e Apoio execuo dos PDM). +Antnio Guterres foi o primeiro convidado de uma srie de debates com lderes polticos que o Inesc est a promover. +Ao falar no Porto, mas ligado a outros pontos do pas graas s inovaes tecnolgicas, o dirigente socialista defendeu ontem as suas ideias na matria: ideias orientadas para uma cincia e uma investigao com qualidade e relevncia para os problemas do pas e contra a aliana entre a mediocridade e a inveja. +Ainda no me tinha lembrado da convenincia de levar uma sandesitas e uma garrafa de gua, quando fui confrontado com a notcia de que tal ousadia era proibida. +Antevi-me, com ou sem sandes, a ser interpelado -- ou mesmo revistado -- por um qualquer segurana imbudo de uma autoridade disparatada. +Imaginei a quantidade (e a qualidade!) de conflitos que essa rasqueirice iria provocar. +E no gostei. +Depois, contaram-me que, em horas normalssimas, o acesso a certos locais -- como o passeio martimo, que me dizem ser dos mais aprazveis -- passava a ser proibido, e guardado pelos correspondentes seguranas, sem que os prprios soubessem explicar porqu. +Conheo o gnero, e no aprecio. +Esta medida foi conseguida com a colaborao activa da Associao dos Operadores Porturios do Sul (AOPS) e da Associao dos Agentes de Navegao e Empresas Operadoras Porturias (Anesul), e insere-se na poltica de modernizao do sistema de informaes da comunidade porturia em Setbal. +Sporting-Vitria de Setbal e Martimo ou Ovarense-FC Porto. +Ontem, em Lisboa, foi este o resultado do sorteio das meias-finais da Taa de Portugal, a jogar dia 9 de Maio. +Uma tiragem de sortes que afastou os dois grandes ainda em prova, mas que leva o Sporting a receber uma equipa que lhe roubou, em Alvalade, um ponto para o Nacional, enquanto o FC Porto poder ir Madeira defrontar a nica equipa com quem sofreu uma derrota para o Nacional nesta temporada. +Ainda assim, no final todos ficaram contentes. +J antes do sculo XIV se havia iniciado o captulo da histria que une Portugal famosa Flandres, cuja referncia povoa os nossos livros de Histria do secundrio, marcando a memria com nomes quase mticos, como o de Anturpia. +Muito mais fantstico do que essas menes soltas da realidade actual saber que as cidades mgicas ainda existem, mesmo que por detrs de arquitecturas e modas no trajar que j nada tm a ver com os tempos em que se bailava a compassos diferentes e em que as ruas se pavimentavam com pressas diversas das nossas. +O plano tem vindo a ser executado gradualmente, atravs de intervenes que integram ou reintegram funes antigas. + exemplo disso a inaugurao de uma nova residncia paroquial em Novembro de 1996, o primeiro passo no sentido da reafectao de espaos do monumento. +Fundado no sculo VI, em pleno domnio suevo, o Mosteiro de Santa Maria de Tibes foi posteriormente arrasado, datando a sua reconstruo do ltimo tero do sculo XI. +A partir da, foi sucessivamente remodelado e aumentado e, tal como hoje existe, o resultado de campanhas de reconstruo e ampliao levadas a cabo nos sculos XVII e XVIII, respondendo actualmente a uma linguagem mista entre o primeiro barroco e o rocaille, explica o Ippar. +P.S. -- PROJECTO DE DIPLOMA e Portaria vm em maisculas no texto, mas escola vem em minsculas! +A ESCOLA de Excelncia escreve-se em maisculas. +O vosso subconsciente anda a trair-vos! +Os gestores de topo das empresas japonesas esto a aceitar o corte dos seus salrios, em face da sbita recesso que se abateu sobre a economia do pas, impondo-se um teste prtico quanto solidariedade que caracteriza a imagem das empresas japonesas no exterior, e tentando contribuir desta forma, para que as firmas que administram possam suportar a crise actual. +Uma vez mais o pblico primou pela ausncia nas bancadas do Autdromo do Estoril, tornando quase secretas as quatro corridas ontem disputadas. +Como que a sentirem a falta de testemunhas, as provas tambm no foram particularmente emocionantes, com as vitrias decididas muito cedo. +No Trofu Renault Clio 16V Vtor Lopes comandou de princpio a fim, construindo uma slida liderana. +Lopes terminaria com 10,017'' de avano sobre Jos Joo Magalhes e 12,378'' sobre Celestino Arajo. +Na Frmula Ford, Carlos Azevedo (Swift) regressou s vitrias, terminando com uma vantagem de 2,328'' sobre Rui guas (Van Diemen) e 3,040'' sobre Frederico Viegas. +Quanto ao Trofu BMW 320iS, Jorge Petiz ultrapassou o seu irmo Alcides a meio da corrida para obter uma vitria fcil, com 2,382'' de avano, deixando o 3., Antnio Barros, a 4,788''. +O portugus Pedro Chaves (Lola / Cosworth) terminou ontem a corrida de Frmula 3000 disputada em Hockenheim no 13. lugar, parando logo a seguir meta com o motor partido. +O piloto portugus fez um bom arranque do 15. posto da grelha, ganhando quatro lugares. +Depois comecei a ter problemas com a embraiagem e com o motor que aquecia muito. +A partir de meio da corrida, o motor comeou a perder potncia e partiu na ltima volta, j perto da meta, contou Chaves. +Cado, entretanto, no 12. lugar, o portugus acabou mesmo por perder uma posio na derradeira volta. +A vitria foi para o italiano Luca Badoer (Reynard / Cosworth) que dominou a corrida de princpio a fim, assumindo tambm o comando do campeonato. +ltimo teorema de Fermat: agora de vez? +Andrew Wiles, o investigador britnico que anunciou prematuramente, no ano passado, ter demonstrado o ltimo teorema do matemtico francs do sculo XVII Pierre de Fermat, talvez tenha finalmente levado este trabalho a bom termo -- noticiou o dirio New York Times na semana passada. +Rui Vilar, ex-comissrio da Europlia 91 e ex-presidente do conselho de administrao da Caixa Geral de Depsitos (CGD), vai ter uma tarefa difcil. +O patrimnio da Gulbenkian valiosssimo, est estimado em cerca de 260 milhes de contos, a valores de 1994, mas a desvalorizao do dlar nos ltimos trs anos tem afectado significativamente os rendimentos que permitem a sua sobrevivncia. +A administrao sempre foi bastante conservadora nas suas aplicaes de capital, que se traduzem principalmente por investimentos no sector do petrleo e pela gesto de uma carteira de ttulos no valor de dois bilies de dlares nos Estados Unidos, composta por aces e obrigaes. +S no ano de 1994, o patrimnio da Fundao diminuu, em valor, cerca de 30 milhes de contos por causa das perdas cambiais do dlar contra o escudo. +Recorde-se que as receitas so obtidas na sua quase totalidade em dlares, que depois necessrio converter para a moeda nacional. +Com O Fim, as rplicas cruzadas formam uma rede ainda mais inextricvel, um verdadeiro concerto desconcertante, mas afinado, em que, de vez em quando, as palavras parecem disparos, exploses, rajadas de metralhadora. +Parecem. +Mas quem no se fiar nestas aparncias descobrir que, nesta amlgama de sonoridades informes -- ou nesta acumulao de nuvens (cinzentas) com formas caprichosas -- toma forma um discurso feito de premonies, ameaas, troves e relmpagos, mortes anunciadas. +Um clima abafado, prenncio de catstrofes. +Quando, na primeira cena, Tom -- amigo e cmplice de Mateus no assassnio da moa -- fala do macaquinho que um amigo lhe vai trazer de Angola, est a falar da arma do crime, do macaco com que Mateus matar Sandra. +E, a propsito do macaco, Mateus sugere que, em vez disso, tragam um tigre. +A, comeam a instalar-se nos dilogos as alegorias da caa, do caador e da presa, do predador, dos carnvoras e dos omnvoros, da comida que se devora -- alegorias que dominam toda a pea, evocando a priori ou a posteriori o crime que o ponto de chegada do espectculo. +Assim, todas as palavras com que Mateus (Manuel Wiborg) contribui para as conversas de caserna anunciam ou recordam o estupro e o assassnio de Sandra, bem como o suicdio com que ele se vai autopunir no final do espectculo. +Apesar dos esforos desenvolvidos pela presidncia luxemburguesa para conseguir o acordo da totalidade dos estados membros, a Itlia manteve a sua oposio ao acordo final em sinal de protesto pela reduo da quota do leite. +No possui hoje Vendas Novas praa de toiros, razo por que a corrida aconteceu numa desmontvel, mas Antnio Morais, no seu livro A Praa de Toiros do Campo Pequeno, ao referir-se s outras praas do pas, diz que Jos Valrio construiu uma praa em Vendas Novas em 1862, tendo nela havido festas de toiros at 1875, ano em que soldados da Escola Prtica de Artilharia, por descuido, provocaram o seu incndio. +Regista ainda na terra a existncia de uma praa nos anos 20 com capacidade para 3400 espectadores, que julgamos ter sido a que existiu at h cerca de 20 anos. +Desta feita, redimiu-se a Escola Prtica, ajudando montagem da praa instalada no campo da feira e nela fizeram as cortesias Jos Maldonado Cortes, Nuno Pardal, o praticante Jos Francisco Cortes e o amador Jos Soudo, a quem saudamos o regresso aps convalescer do gravssimo percalo que lhe aconteceu na praa da Malveira. +Lidou-se um curro de toiros de Jos Lus Sommer de Andrade, que o Grupo de Forcados amadores da Moita pegou. +Acompanhando o comportamento dos principais mercados asiticos, a Bolsa de Sidney terminou a sesso de ontem em alta pronunciada. +Para os operadores, tambm aqui as valorizaes se ficaram a dever reentrada de novos investidores, nomeadamente fundos de investimento internacionais. +O ndice AOI encerrou nos 2034 pontos. +Nesta classificao, destaque para nova subida do sueco Stefan Edberg, que surge na 20 posio, graas presena na final de Queen's (700 mil dlares). +Recorde-se que Edberg, antes de Roland Garros, estava no 45 lugar, mas as excelentes exibies na terra batida parisiense e na relva londrina permitiram-lhe o salto. +O alemo Boris Becker, vencedor do Queen's, ao derrotar Edberg por 6-4, 7-6 (7-3) 11 anos depois de ter conquistado no mesmo local o seu primeiro ttulo em relva, manteve a quarta posio mas diminuiu a diferena pontual para Andre Agassi. +Em Rosmalen (500 mil dlares), na Holanda e tambm em relva, o vencedor foi o norte-americano Richey Reneberg, que bateu na final o francs Stphane Simian por 6-4, 6-0. +Espectculo de um jovem grupo italiano, integrado no esprito das mais novas tendncias teatrais europeias. +Cerimnia e combate em que a mstica grotowski-barbiana, as artes marciais orientais, a msica e a poesia italiana e um projecto intercultural se cruzam em boa harmonia. + Depois de Montemor, vora, Beja, Coimbra e Braga, a vez de a Grande Lisboa o ver na Damaia. +Ocasio nica. +SALA D. JOO V Largo da Igreja. +Dom., 15, s 21h45. +O actor e realizador norte-americano Clint Eastwood, 63 anos, presidente do jri do Festival de Cannes (ver p. 28), defendeu ontem a entrada de uma maior variedade de filmes estrangeiros no mercado dos Estados Unidos. +Em conferncia de imprensa dada conjuntamente com a vice-presidente do jri, a actriz francesa Catherine Deneuve, 50 anos, Eastwood confessou que, em vez de sanes de proteccionismo, preferiria que uma maior variedade de filmes franceses, asiticos, russos entrasse nos Estados Unidos. +Para alargar o horizonte do pbico norte-americano, explicou. +O actor disse estar a produzir The Stars Fell on Henrietta, um filme realizado por James Keach, com Robert Duvall, e projecta rodar Golf in the Kingdom, que tem por tema o golfe, um desporto de que se confessa adepto. +Como actor dever desempenhar o papel de um fotgrafo em The Bridge of Madison County, uma pelcula a realizar pelo australiano Bruce Beresford com base no best seller homnimo de Robert James Weller. +Os Utah Jazz voltaram a provar que so os visitantes mais antipticos da Liga Norte-Americana de Basquetebol profissional (NBA), ao averbarem na jornada de sexta-feira o seu 11 triunfo fora de casa, melhorando o seu registo da Liga na presente temporada para 11v/4d. +Desde Maio deste ano que Joaquim Correia, escultor natural do concelho da Marinha Grande- que doou os seus trabalhos para integrar o ncleo do museu no Convento de Santo Agostinho- chegou mesmo a ameaar desvincular-se do compromisso com a autarquia, caso o processo no fosse desbloqueado at ao final do ano. +Em declaraes ao PBLICO, Joaquim Correia afirma manter a sua posio, apesar de considerar positiva a iniciativa agora tomada pelo IPPAR, que lhe ter sido comunicada pela prpria Cmara de Leiria na passada sexta-feira. +Joaquim Correia justificou a sua posio cautelosa sobre o assunto, adiantando que o processo de reconverso do Convento de Santo Agostinho em museu se arrasta h oito anos. +Quem estraga velho paga novo, disse Vtor Melcias, afirmando pretender que sejam repensadas as situaes onde j houve entregas apressadas de antigos edifcios das Misericrdias, ocupados at h bem pouco tempo por unidades hospitalares do Estado. +Segundo Vtor Melcias, at aos anos 70, as Misericrdias dedicavam cerca de 90 por cento da sua actividade a aces na rea da Sade. +A ocupao dos edifcios das Misericrdias por unidades hospitalares do Estado conduziu, nas ltimas dcadas, reconverso das reas de interveno das Misericrdias portuguesas, que tm actualmente na assistncia social o seu principal campo de aco. +Ainda se podem queixar se quiserem ..., gracejou Raposo, aps entregar o documento. +Foi por entre elogios unnimes dos deputados sua actuao e a manifesta disponibilidade para dela retirar os necessrios ensinamentos que Mrio Raposo deixou ontem o Parlamento onde foi formalizar e explicar as razes da sua renncia ao cargo de Provedor de Justia. +Poucos minutos depois do final da reunio que, durante cerca de trs horas, juntou o Provedor demissionrio e os deputados da Comisso Parlamentar de Direitos, Liberdades e Garantias, era divulgado um comunicado onde se reala a actuao de elevado mrito do dr. Mrio Raposo, marcada por critrios de independncia na defesa dos direitos dos cidados perante a Administrao, dela se devendo retirar os necessrios ensinamentos para que sejam criadas as adequadas condies institucionais que permitam a normalidade e plenitude do exerccio das suas funes. +Jean Alesi obteve ontem a pole position provisria para o Grande Prmio de Espanha, que se disputa amanh no circuito da Catalunha (Barcelona). +O piloto francs foi o mais rpido na primeira sesso de treinos de qualificao, superando por 35 centsimos de segundo o seu companheiro de equipa Gerhard Berger. +A Ferrari conseguiu assim colocar os seus dois carros na primeira linha da grelha provisria, um resultado que motiva toda a equipa. +Depois do segundo e terceiro lugares no GP de San Marino, h duas semanas, e dos bons resultados conseguidos nos testes realizados nos dias seguintes, tambm no circuito de mola (Berger foi o mais rpido dos 16 pilotos que estiveram em pista, batendo Schumacher e Hill), a Ferrari mostra em Barcelona que a sua competitividade est a aumentar, reforando a ideia de que se poder bater em igualdade de circunstncias com a Benetton e a Williams, as duas equipas motorizadas pela Renault. +O Gerhard tem razo, ele tem os ps bem assentes no cho. + preciso no esquecer que estes so os treinos de sexta-feira, no os de sbado. +Entretanto, Bernardo Vasconcelos, mdico do Benfica, confirmou ontem que, na sequncia do ocorrido no ltimo Benfica-Sporting, Joo Pinto ser operado, embora ainda no se conhea exactamente a extenso da leso. +Como h dvidas se existem roturas totais ou parciais dos ligamentos, o futebolista far agora uma artroscopia e s depois se saber se h necessidade de fazer uma ligamentoplastia. +Tambm no se sabe quando ou onde ocorrer a interveno cirrgica. + informao reservada, a pedido do jogador. +Um guineense e uma portuguesa que se dedicavam a angariar trabalhadores para o Kuwait, cobrando uma taxa de dez contos, foram detidos pela Polcia Judiciria. +Assim, a Liga nem sequer pode comprovar que de facto est a ser feito um inqurito, sublinhou Pedro Vieira. +Quanto aos 75 mil contos que a Secretaria de Estado diz agora ter disponibilizado para a operao de salvamento do peixe, ningum sabe onde, como e por quem foram utilizados, at porque das centenas de toneladas ali existentes apenas se salvaram cerca de duas dezenas. +Cumprindo a promessa de se deslocar ao Porto na primeira tera-feira de cada ms, para reunies de trabalho com os responsveis autrquicos e os agentes culturais da cidade, o ministro recebeu ontem, entre outros, o director do Teatro Nacional S. Joo, Ricardo Pais, a responsvel do Teatro Rivoli, Isabel Alves Costa, e ainda representantes do Teatro Art'Imagem e do Museu de Imprensa, cuja primeira fase ser inaugurada pelo Presidente da Repblica na prxima sexta-feira. +s j habituais crticas poltica cavaquista no domnio da cultura, que considerou economicista e frvola, Carrilho acrescentou alguns ataques ao actual lder do PSD. +Neste ltimo ano, o que que o PSD props em matria cultural?, perguntou o ministro, aludindo ao primeiro aniversrio da liderana de Marcelo Rebelo de Sousa, que se celebrava mesma hora em Santa Maria da Feira. +A Food and Drug Administration (FDA) -- a agncia americana que fiscaliza os medicamentos e os alimentos -- pode passar a regulamentar o tabaco, em virtude dos efeitos biolgicos desta substncia. +Quem o decidiu foi o juiz William Osteen, na sexta-feira passada, naquela que foi considerada a mais devastadora derrota da indstria tabaqueira nos Estados Unidos. +A deciso significa que vai vingar a inteno da FDA de proibir a venda de tabaco a menores, obrigando as lojas a pedir os bilhetes de identidade para o efeito. +Ser tambm FDA que caber regulamentar, a partir de agora, as mquinas de venda de tabaco. +Num pas em que -- ainda segundo estatsticas das Naes Unidas -- mais de metade da populao tem menos de 15 anos, foi estranho para muitos observadores que o nmero de eleitores potenciais se estimasse em 5,8 milhes, para uma populao total calculada em dez. +Vrios ajustamentos tm sido feitos ao longo desta maratona, como no Bi, em que o nmero potencial de eleitores passou de 577 para 700 mil. +Tambm em Benguela e no Cunene houve alteraes. +O que se sabe, concretamente, que neste momento o nmero de portadores do carto de eleitor em Angola j ultrapassou os 4,3 milhes e o alargamento do prazo de registo at 10 de Agosto permitir recensear as pessoas que vivem nos lugares mais remotos do territrio. +Esta situao demonstra que o tema ambiente est na moda. +Quando a obra foi feita pela primeira vez, h uns cinco anos, no aconteceu nada ao responsvel. +Agora que s se veio agravar um mal que j estava feito que as pessoas se interessaram, afirmou Joo Mendona, adiantando que o Posto 7 no tem nada que ver com a destruio da duna e esclarecendo que o bar no est a ocupar ilegalmente a praia -- como sups um funcionrio da Junta de Freguesia do Castelo, a que a praia do Moinho Baixo pertence. +A licena de ocupao de domnio pblico martimo foi emitida pela DRARN de Lisboa e Vale do Tejo em Maio passado. +O Governo Civil tambm autorizou o funcionamento do bar. +Neste momento, a DRARN no est a conceder novas licenas de ocupao de domnio pblico, mas tem renovado licenas antigas, segundo Vitria Bruno da Costa. +Isto porque vo ser aplicados Planos de Ordenamento da Orla Costeira (POOC), pelo que no vale a pena estar a autorizar investimentos que podem vir a estar em desconformidade com os POOC, explicou a directora. +No cantinho da bancada central que estava habitvel havia algumas caras conhecidas. +Todas do Benfica, claro, que divide muitos jogadores com o seu clube-satlite. +Mrio Wilson foi o primeiro a chegar. +Depois, sentaram-se o guarda-redes Veiga e o belga Lucien Huth, treinador dos guardies encarnados. +O Alverca foi feliz e conseguiu o golo logo aos 4'. +Foi na sequncia de uma jogada bonita de Ral, que subiu bem e deu para Ramirez. +O mais novo reforo do Benfica -- do Alverca, no caso -- centrou bem e, na rea, Akw cabeceou para dentro da baliza. +Se a primeira volta das eleies foi marcada por um inenarrvel caos organizativo, a segunda correu relativamente bem. +As chuvas atrasaram as operaes em algumas regies. +Mas o grande motivo do atraso no anncio dos resultados parecia ser a proximidade dos nmeros. +Na segunda-feira noite, segundo os dados parciais da CNE, Nino tinha uma vantagem tangencial. +a vontade de mudana dividiu ao meio o eleitorado, algo de impensvel meses atrs. +Um tunnelier uma autntica fbrica debaixo do solo. + composto por uma cabea escavadora, parecida com uma roda dentada, com pontas de carboneto de tungstnio. + este material super-resistente que permite roer a rocha mdia de duas a trs voltas por minuto e avanar velocidade vertiginosa de 200 metros por semana. +Em cada volta desta roda avana-se 10 centmetros e so trucidadas cinco toneladas de rocha. +Depois da cabea vem uma autntica fbrica ambulante que tritura as pedras e as envia para o exterior, ao mesmo tempo que vai enfiando uns aros em beto armado, impermeveis e resistentes a uma presso de 200 bars, que revestem o tnel. +O tempo da dinamite, p e picareta j acabou h muito. +Estes monstros so comandados e controlados por meios electrnicos e informticos. +Tinha que ser bonito, invocaram os responsveis pelo projecto Y, a nova arma compacta da Lancia para o fim do sculo, mais um dos refinados produtos do design italiano, ainda que sado dos esboos de um engenheiro. +Maior que o seu antecessor, o Y10, o Y assume formas tridimensionais e curvilneas, sempre exclusivas e eternamente femininas. +Esta cumplicidade de conversa constante entre vtimas da mesma sociedade -- na definio de Liberto Cruz -- doou ao teatro de marionetas o infortnio e o orgulho da marginalidade. +Bonecos e bonecreiros foram desde sempre perseguidos e interditados, e sintomtico que o dramaturgo Antnio Jos da Silva, O Judeu, tivesse morrido na sacra fogueira da Inquisio -- condenado pelas peas para marionetas que escrevia e representava no seu Teatro do Bairro Alto. +Os esbirros da Inquisio apareciam sempre aos pares, perturbando o Sculo das Luzes, a julgar pela Sala Antnio Jos da Silva do Museu da Marioneta, em Lisboa. +Um par de presenas enlutadas fazem aqui as vezes de personagens das trevas, no meio de uma vitrina de bordados barrocos, azuis debruados e rendas de brilho transparente: os personagens das peas Vida do Glorioso D. Quixote de La Mancha e do Gordo Sancho Pana e Guerras do Alecrim e da Manjerona. +Ambos os textos foram recuperados para a cena pela companhia Marionetas de So Loureno (seguindo textos do Judeu), cujos fundadores so tambm os responsveis pela constituio do Museu -- Helena Vaz e Jos Gil. +Foi anteontem empossado o grupo de trabalho interministerial que vai preparar a articulao entre as bibliotecas escolares e as da rede de leitura pblica. +Chefiada pela escritora Isabel Alada, a equipa, constituda por representantes dos ministrios da Cultura e da Educao, tem trs meses para analisar a situao e fazer propostas. +A administrao pblica no tem capacidade do ponto de vista veterinrio para fiscalizar e punir os prevaricadores. +Nos termos do novo regulamento, devem ainda ser destacadas algumas inovaes. +Estabelece-se que no h lugar suspenso ou demisso do mdico veterinrio coordenador ou dos executores no decurso do programa sanitrio anual, a no ser por motivo de fora maior, devidamente justificado e aceite pela direco regional de agricultura, com posterior homologao pela Direco Geral de Veterinria. +Consagra-se tambm o direito desta ltima efectuar visitas de inspeco e auditoria tcnica s OPP e exploraes nelas integradas, bem como aos produtores individuais, impor as correces tidas como necessrias ou propor medidas sancionatrias. +Antes do relator-geral, falaram o cardeal Angelo Sodano, prefeito da Congregao para os Religiosos e os Institutos Seculares, e o arcebispo Jan Schotte, secretrio-geral do Snodo, que fizeram a sntese dos trabalhos preparatrios. +Durante esta semana e parte da prxima, os cerca de 350 participantes -- bispos, superiores de congregaes religiosas masculinas e femininas, peritos e auditores -- estaro reunidos na aula sinodal, ou sesso plenria. +As intervenes so feitas com base nos temas e contedos do documento de trabalho, elaborado precisamente em resultado do processo preparatrio. +Depois desta fase, os participantes dividem-se em crculos menores, por grupos lingusticos, de modo a produzir sugestes para os documentos finais. +Perdoa-nos, Erich Honecker era o ttulo de primeira pgina do antigo jornal do Partido Comunista sovitico, Pravda, segundo o qual os actuais dirigentes da Rssia teriam aberto um precedente para si prprios ao entregar o seu antigo amigo e camarada. +A mulher de Honecker, Margot, abandonou ontem a embaixada chilena em Moscovo, tendo seguido directamente para Santiago do Chile, onde vive a sua filha Sonya, e no para Berlim como chegou a ser anunciado. +Margot, a quem os alemes de leste chamavam de bruxa, foi ministra da Educao da ex-RDA e actualmente est a ser investigada por suspeita de ter forado vrios dissidentes polticos a entregarem os seus filhos para adopo. +O professor Marcelo Rebelo de Sousa, ao exigir o referendo nacional, est a facilitar imenso a resoluo de to momentoso problema. +No estamos esquecidos de que o eng Guterres fez da sua promessa da criao das regies administrativas uma das principais bandeiras da sua campanha eleitoral. +Queremos que cumpra a promessa, mas de maneira digna e sensata. +O tempo, quente e hmido, impediu a obteno de grandes marcas, mas no foi obstculo ao domnio fcil de Pinheiro sobre os seus mais directos adversrios -- Pedro Pessoa, do Bairro Santiago (31m31s), Lus Vieira, da AA Peniche-ptica 2000 (31m55s) e Carlos Almeida, da UR Dafundo (31m57s). +Em senhoras, Paula Laneiro tambm no teve dificuldades em bater Umbelina Nunes, do Casal Privilgio (267 da geral e primeira veterana), e Lusa Almeida, do Cruz Quebradense (295). +inicialmente, apareceu na lista de classificao como sendo Octvio Sousa, do Super Estrelas. +Isto deve-se possivelmente ao facto de Pedro Pessoa ter feito a prova com dorsal trocado. +Casos semelhantes a este so frequentes nas corridas de estrada que se efectuam em Portugal e dificultam e falseiam a elaborao das classificaes individuais e colectivas. +Magalhes Mota, que se confessou muito perturbado ao tomar conhecimento da morte do seu grande amigo, evocou um encontro de ambos, h cerca de um ms, na Buchholz, livraria lisboeta que o professor Miller Guerra frequentava assiduamente. +R. -- No. +Peo-lhes para desligar e tentar nova ligao porque esto a invadir a privacidade do meu auscultador. +P. -- Um amigo precisa de emprego e concorre sua empresa. +Outra pessoa com melhor currculo candidata-se ao mesmo emprego. +D o emprego ao seu amigo? +P. -- O pay-tv chega tarde a Portugal? +R. -- Chegou um pouco atrasado porque Portugal comeou mais tarde com a rede de cabo. +Mas se tivermos em conta a nossa dimenso, ningum em to pouco tempo cobriu o pas como ns. +Se estamos atrasados, um ano ou dois. +O incidente de ontem grave mas no o primeiro. +Vrias vezes, nos ltimos dois meses, afegos e iranianos trocaram acusaes sobre violaes do territrio ou do espao areo, alegaes nunca confirmadas por fontes independentes. +Em Agosto, por exemplo, Teero anunciou uma troca de tiros, que ter sido efmera e com um nmero indeterminado de vtimas, sem conseguir no entanto provar uma agresso. +Lus de Matos apresenta 45 minutos de magia com Pequenas Grandes Iluses. +A msica do Chile pelo grupo Alturas . +A nossa participada A. DE MATOS atingiu uma facturao de cerca de 1.150.000 contos, mais 48% do que no ano anterior. +Na sua actividade tomou um peso importante a venda de produtos transformados, na linha de estratgia que fixmos. +O mau tempo atrasou consideravelmente as operaes, disse Ajit Vardi, um dos responsveis da coordenao das operaes em Bombaim, capital do estado de Maharashtra. +Entretanto, a ndia rompeu com a sua tradio, aceitando com gratido a ajuda material estrangeira. +Recusou, contudo, as propostas dos governos estrangeiros de enviar para o pas equipas de especialistas. +O Governo indiano considera que dispe de meios suficientes para realizar com sucesso as operaes de socorro, mostrando-se reticente, segundo um responsvel da distribuio da ajuda internacional, no que respeita a receber pessoal estrangeiro. + Equipas propostas pela Rssia, ustria, Itlia, Alemanha e Sua estavam a postos para partir para a ndia. +Mas o estado de alerta foi suspenso no sbado com a justificao de que j era demasiado tarde para encontrar sobreviventes e que a ndia no fizera um apelo oficial, considerando suficientes os seus prprios recursos. +Em relao situao da Fonte da Telha e s demolies que ali j se efectuaram, Maral Pina afirma no saber explicar por que que parte do entulho se mantm no local, remetendo a explicao para o Ministrio do Ambiente. +Ministrio que, alis, acusa de ser culpado de parte dos problemas que se vivem na Costa da Caparica. +O vereador do Ambiente da Cmara Municipal de Almada, CMA, Jos Lus Leito, por seu turno, refere que a limpeza das matas que envolvem a zona da Fonte da Telha, que feita, dentro do possvel, pela autarquia, deveria ser competncia do Instituto de Conservao da Natureza, ICN, pois a rea est sob a sua alada, atravs da tutela do Ministrio do Ambiente. +-- The Edge, realizado por um neozelands em Hollywood, Lee Tamahori, e baseado numa pea de David Mamet, acabou de estrear.The Edge, realizado por um neozelands em Hollywood, Lee Tamahori, e baseado numa pea de David Mamet, acabou de estrear. +Junta Anthony Hopkins e Alec Baldwin no Alaska, depois de um desastre de avio. +Hopkins um bilionrio, e pensa que Baldwin, um fotgrafo de moda, est a dormir com a sua mulher, Elle MacPherson, top model a tentar que a sua carreira cinematogrfica pegue. +Hopkins diz que, ao contrrio dos seus filmes anteriores, que eram to divertidos quanto ver tinta a secar e bons exerccios de disciplina e economia e tudo isso, este um filme do tipo que lhe apetece fazer agora -- aco. +-- U-Turn, de Oliver Stone, com Sean Penn, tambm acabou de ser distribudo nos ecrs americanos. +Penn tem um problema no seu carro que o obriga a parar numa pequena cidade de loucos no meio do Arizona. +De l no vai conseguir sair. +No vai ser um filme que obrigar Stone a pedir desculpa a dezenas de pessoas nas prximas dezenas de anos, como ele ainda hoje anda a fazer depois de J.F. K . e de Assassinos Natos. +Os procedimentos de registo, complicados e morosos, devem s por si afugentar muitos ciganos, j de si com baixos nveis de alfabetizao. +E os interessados tm de apresentar s autoridades checas documentos obtidos em trs reparties eslovacas diferentes. +O objectivo da lei livrarmo-nos de habitantes inconvenientes, explica Michal Pulo, presidente da maior organizao de ciganos da Repblica Checa. +Jorge Sampaio formaliza a sua candidatura Presidncia da Repblica, na Reitoria da Universidade Clssica de Lisboa. +Terceiro aniversrio da assinatura do Tratado de Maastricht pelos ministros dos Negcios Estrangeiros e das Finanas da Comunidade Europeia. +A soluo encontrada parece ser a do encaminhamento da populao escolar para os centros de sade, entregue aos cuidados do respectivo mdico de famlia, mas capacitando estes servios do seu papel crescente na sade escolar. +Se no houver, de facto, uma substancial alterao na conduta dos centros de sade nesta rea, a sade escolar tender a ser o que j fora dos concelhos de Lisboa, Coimbra e Porto: a medicina curativa das urgncias hospitalares, a que os pais recorrem quando o filho est doente. +Representantes das 60 famlias que ocupam ilegalmente casas na urbanizao do Vale de Arcena, delegados do Centro Regional de Segurana Social, responsveis da Cmara Municipal de Vila Franca de Xira e da empresa proprietria dos fogos, a Eurocapital, reuniram-se, na sexta-feira, naquela cidade, para discutirem a forma como ser feito o levantamento individual que determinar quais os moradores que ali podero continuar a residir no caso de terem disponveis as verbas pedidas por cada casa. +Nesta avenida de luxo, cortada longitudinalmente por um canal, perfilam-se, de um lado, os bancos (alm dos alemes, cerca de cem bancos estrangeiros operam na capital da Vesteflia); do outro, os estabelecimentos comerciais -- Cartier, Armani, Gucci, Van Cleef, etc. +Ourivesarias que expem nas suas montras conjuntos de pulseiras e anis com preos superiores a meia dzia de milhares de contos, casas de moda com casacos de homem a custar mais de duas centenas de contos. +Cheira a dinheiro em Dsseldorf, uma cidade em que, mesmo nas horas de ponta (e apesar do trfico intenso), as nicas bichas so as dos imigrantes turcos junto ao seu consulado. +Uma cidade onde at h carros de lixo com monitores de televiso em vez de espelhos retrovisores. +Jos Gregrio (Grego) o novo campeo nacional de surf, ttulo conquistado durante a II Semana Radical da Beira Litoral, que terminou no ltimo sbado na Figueira da Foz. +O surfista da APSSOC ganhou a finalssima, onde estiveram os oito primeiros no conjunto das duas mangas. +A APSSOC, com trs atletas nos quatro primeiro lugares, a campe nacional de clubes. +Entretanto, est a disputar-se a Taa de Portugal, competio por equipas, na praia Grande, at o dia 12 de Agosto. +Paulo Silva (Fluorescente) foi o vencedor do Campeonato Nacional de bodyboard, que tambm se realizou na Figueira da Foz. +Apesar do quinto lugar nas duas mangas, Paulo Silva, da Quinta dos Lombos, venceu a finalssima. +O Surfing Clube de Portugal, de So Pedro do Estoril, venceu por equipas. +Na praia Grande, j terminou a Taa de Portugal de bodyboard, disputada por equipas e a vitria coube ao Surfing Clube da Caparica. +Subir ao monte Garizim, no primeiro dia de neve do ano, quando as pedras e os abetos se escondem sob um manto de neve, ascender ao paraso, to grande a beleza celestial da paisagem. +Mas Shalom Cohen quer descer do cume da montanha dos bem-aventurados para um lugar mais terreno. +O seu desejo sentar-se no Conselho Palestiniano, onde Yasser Arafat reservou uma cadeira para um dos trs candidatos do mais pequeno e mais antigo povo do mundo -- os samaritanos. +So cerca de 600 almas, 284 a viver em Nablus, a maior cidade da Cisjordnia, e 297 a residir em Holon, prximo de Telavive. +Arafat sempre defendeu um Estado multitnico e, ao apoiar os descendentes de uma das tribos de Israel, a de Levi, filho de Jacob, ele mostra que tambm os judeus sero bem-vindos numa futura Palestina. +Alm disso, ao proteger os samaritanos de Nablus, mostra que vai mais longe do que o Estado hebraico, que no d aos de Holon a oportunidade de estarem representados no Knesset (parlamento). +A Espanha vai investir at ao fim do ano 67,6 mil milhes de pesetas em infra-estruturas ferrovirias. +Mas, aps o esforo destinado ao Sul, com a linha de alta velocidade Madrid-Sevilha, em 1992, as prioridades de 1995 apontam para o Leste e Norte. +O chamado corredor mediterrneo (Valencia-Barcelona) e a linha de alta velocidade Barcelona-Narbonne representam metade do investimento total em infra-estruturas de transporte ferrovirio. +A UNITA declarou ontem que o seu lder, Jonas Savimbi, no aceitar o cargo de vice-presidente que lhe proposto nos acordos de paz de Angola, preferindo viabilizar um Governo de unidade nacional desempenhando o papel de lder de uma oposio leal, com direito a ser consultado pelo Presidente sobre todas as questes nacionais. +As pessoas no MPLA temem que a UNITA queira romper o processo, estando deliberadamente a provocar atrasos at Novembro, data em que expira o mandato do Governo, disse Reuter o chefe dos negociadores da UNITA em Luanda, Isaas Samakuva, explicando que tinha entregue ao Governo e ONU um documento com propostas concretas. +Mas os atrasos correspondem s dificuldades que temos enfrentado, acrescentou Samakuva. +O Governo tem capitalizado sobre o que ns no fizemos, o que s contribuiu para enfraquecer a liderana da UNITA, dificultando-lhe o cumprimento das tarefas restantes. +A segurana a antecipao a todos os nveis para ver, ao longe, e ser-se capaz de tudo prever -- um mal-estar, uma sacanice dos outros. +No conheo um s piloto que no conduza sua velocidade de segurana. +Segundo o artigo 1 do Cdigo da Estrada, aquela que permite ter o domnio da viatura e de parar ao mnimo impondervel. +Baseado na sua experincia e em testes realizados sob controlo mdico, Jean-Pierre Beltoise afirma que a maior parte dos condutores no tem a menor necessidade de parar de duas em duas horas. +Antes pelo contrrio: uma paragem maadora pode fazer baixar a vigilncia. +Em contrapartida, se se sentir com dores nas costas ou se um enorme desejo de ir apanhar malmequeres o estiverem a massacrar, uma pausa relanar a energia. +Uma cochilada? +Tudo bem, se for capaz de passar em poucos minutos da viglia ao sono e vice-versa. +Especialistas em luta anti-terrorista e no complexo puzzle de grupos e grupsculos afegos identificam partida duas organizaes como as mais susceptveis de fornecerem treino a jovens idos de outros pontos do mundo. +E isto porque possuem um conceito internacionalista da guerra santa contra o Ocidente. + o outro o Markaz Al-Dawat, organizao extremista wahabita. +Os lees de Faro e de Lisboa lutaram ontem muito no Estdio de So Luiz, mas no conseguiram marcar. +Os da casa esto ainda muito longe de uma boa forma fsica e a partir dos 25 minutos do primeiro tempo refugiou-se quase sempre na defesa. +O Sporting dominou, alguns jogadores voltaram a mostrar pormenores interessantes, mas faltou-lhes inteligncia no futebol ofensivo. +Um empate que acaba por ser justo, para duas equipas que precisam ainda de ser muito trabalhadas. +O Sporting iniciou o jogo com o habitual 4x4x2 de Octvio, mas com algumas novidades em relao temporada passada. +Saber ocupou a direita da defesa, enquanto l mais na frente Yordanov jogou na esquerda do ataque, cabendo a Hadjy alinhar no meio no apoio ao avanado Leandro, ficando a direita a cargo de Pedro Barbosa. +Lang subiu muito no terreno, ficando Oceano sozinho no apoio defesa, facto que valorizou o futebol ofensivo dos lees. +Alberto Joo Jardim foi a Porto Santo para conferir posse ao delegado do Governo Regional na ilha, Jos Rosado. +O dirigente do PSD da Madeira aproveitou a ocasio para dizer que, at 1985, no houve qualquer desenvolvimento empenhado (no arquiplago) por iniciativa de Lisboa. +Para Alberto Joo Jardim, s a autonomia poltica da Madeira e de Porto Santo juntos permitiu grandes saltos nos ltimos anos2 e seria suicdio para Porto Santo se essa solidariedade e unidade fossem quebradas. +O chefe do Governo regional fez apelo ao investimento privado para assinalar que ao sector pblico no cabe resolver todos os problemas da ilha. +Joo Alberto da Rocha Pris, um dos dezanove diplomatas despromovidos pelo recente Acordo do Supremo Tribunal Administrativo, que ps em causa as promoes efectuadas em 1987, apresenta hoje as credenciais ao Presidente angolano Jos Eduardo dos Santos. +O novo embaixador de Portugal em Luanda tem 46 anos, iniciou a carreira diplomtica em 1969 e desempenhava as funes de director-geral para a Cooperao. +No que o sbio das matrizes encontrou, no PSD, entusiastas seguidores? +A morgue do Hospital de Beja foi privatizada! +J s falta a Paz eterna ... +Ainda por cima em Beja, um dos beros do colectivismo em Portugal! +aposto que ele no partilharia os mtodos seguidos pelos seus discpulos do Baixo Alentejo e era bem capaz de organizar concursos pblicos abertos ... +O aval de 600 mil contos do Governo UGT foi posteriormente abordado por Cunha Rodrigues. +O procurador adiantou que j lhe foi entregue um parecer sobre o caso, mas s dever pronunciar-se dentro de 15 dias. +Foi peremptrio, no entanto, ao afirmar que, se o Supremo Tribunal Administrativo declarar a anulao do emprstimo, a central sindical tem de devolver o dinheiro, mesmo que este j tenha sido gasto. +A seleco nacional de futebol e as eventuais agresses a prostitutas durante o estgio efectuado antes do jogo com a Irlanda, no final de 1995, tambm foi abordada. +Cunha Rodrigues diz que tudo est nas mos do procurador do Tribunal de Cascais, de quem aguarda um relatrio para breve. +A extrema direita fez uma campanha eleitoral moderna, servindo-se do processo direct mailing americano enviando os seus panfletos xenfobos, do tipo Hamburgo tem de permanecer um estado alemo, pelo correio, em especial a milhares de jovens eleitores. +E, por ironia do destino, fazem-no a coberto da lei eleitoral hamburguesa que autoriza os servios municipalizados a fornecerem as moradas de qualquer habitante, por cinco marcos cada. +Esta legislao, altamente contestada pelos servios de proteco de dados, no foi alterada a tempo pelas autoridades da cidade. +Uma das coisas que mais me impressionaram foi a desertificao do pas no interior. +E todos os esforos que tm sido feitos no tm dado resultado, disse o Presidente, que desculpou a actuao dos seus governos ao afirmar que hoje temos melhores condies e melhores tcnicos do que tnhamos. +Valente de Oliveira, que falou antes do Presidente, foi hbil ao fazer uma interveno que quase esvaziava o discurso de Soares. +mas estes problemas esto a ser resolvidos. +Quando Borges morreu, a criada apareceu com outro testamento, que era o que desaparecera e que ele tinha procurado antes de partir para Genebra. +Por isso, Borges fez novo testamento. +Um e outro dizem a mesma coisa, no h qualquer problema jurdico quanto a isso, a nica diferena que, no primeiro, ele deixa uma determinada soma a Fanny e, no segundo, essa soma um pouco menor. +Voc fala constantemente daqueles que se dizem amigos de Borges, dos que lhe vinham pedir que escrevesse os seus prlogos, e garante que a esto a acossar. +Quem so? +Os primeiros judeus srios autorizados a emigrar pelo Presidente Assad j chegaram aos EUA. +Em Damasco, outros querem seguir o exemplo, aproveitando este novo privilgio que amanh lhes pode ser negado. +Tchernomirdin est a cozinhar um Governo com o Parlamento. +Sabe que se no chegar a acordo, chumbado. +A Rssia caminha para uma coligao. +A maioria comunista ter uma palavra a dizer. +A nata dos reformistas foi excluda ou auto-excluiu-se. +Os mercados inquietam-se. +Ieltsin garantiu a Clinton que as reformas sero uma prioridade. +Mas o Presidente est cercado por pedidos de renncia. +O Parlamento Europeu condenou ontem a deciso anunciada pela multinacional Seagate de encerrar a fbrica de Palmela, que ir provocar o despedimento de 850 trabalhadores. +A resoluo aprovada pelos deputados europeus foi apresentada pelo grupo da coligao de esquerda e subscrita pelos trs deputados comunistas, Barros Moura, Miranda da Silva e Srgio Ribeiro. +Considerando que a fbrica est tecnologicamente bem apretechada, com elevados nveis de produtividade e com uma excelente situao financeira, os parlamentares insistiram na necessidade da Comunidade e dos estados-membros exigirem aos parceiros comerciais, respeito das condies sociais mnimas, tendo por base as convenes e recomendaes da Organizao Internacional do Trabalho (OIT). +A Frana est em vias de tomar medidas para a concretizao de um embargo comercial ao Haiti, tendo j pedido aos seus parceiros comunitrios que acompanhem Paris na tomada destas medidas. +Um porta-voz do ministrio francs precisou ainda que o Governo est em vias de congelar todos os bens pblicos haitianos. +O ministro dos Negcios Estrangeiros, Roland Dumas anunciou ontem, que a Frana vai aplicar as decises tomadas pela Organizao dos Estados Americanos (OEA). +Decises estas referentes imposio de um embargo comercial, at que o presidente do Haiti, Jean-Bertand Aristide volte ao poder. +a msica verbal parece pedir a correspondncia com a msica do violoncelo, e uma e a outra so associadas a ideias e sentimentos de runa e ruptura, embora a sua produo e perfeio impliquem, talvez, um alvio e uma catarse. +e a sua forma, ou a sonoridade que produz, expressa por imagens bem visveis ou concretas: pontes, arcos, arcadas, balastres; rio, caudais, sorvedouro; barcos, lemes, mastros; urnas, blocos de gelo. +6 Alis, h no poema uma aluso explcita cor (brancos os arcos, para no falar na implcita dos alabastros e dos blocos de gelos) e vrias imagens cinticas: de que esvoaam, por baixo passam, se despedaam, caudais de choro, trmulos astros ... +O rond, ou rondel (como lhe chamou o poeta), Viola chinesa imita e fala de um som lento, montono, fastidioso, idntico ao de uma parlenda ou de uma lengalenga, que todavia repercute subitamente na conscincia do enunciador, ou no seu corao, tornando visvel uma sua cicatriz melindrosa e permitindo a distenso das suas asitas. +A relao cambial entre o escudo e a peseta no tem constitudo matria pacfica nos ltimos meses. +Por um lado esto os empresrios, defensores de uma maior estabilidade cambial entre as duas divisas. +Opinio diferente tm por vezes as autoridades monetrias nacionais, empenhadas em fazer passar a ideia de que o escudo no tem sempre de seguir a evoluo da moeda espanhola. +Mas os agentes econmicos e o mercado consideram que h uma ligao inevitvel entre as duas moedas, enquanto o Banco de Portugal e o Ministrio das Finanas vo reafirmando a autonomia cambial do escudo, embora na prtica sigam parcialmente a peseta. +Este tema alvo de uma anlise no ltimo relatrio da SAER -- Sociedade de Avaliao de Empresas e Risco, entidade ligada ao ex-ministro das Finanas Ernni Lopes. +Para a SAER, a recente apreciao do escudo face peseta poder no ter sido a deciso mais correcta. +Isto porque, nestas condies, e atendendo ainda a que Portugal e Espanha produzem e vendem, grosso modo, a mesma gama de bens e servios, deixar o escudo apreciar-se relativamente peseta reduz a capacidade concorrencial das nossas empresas no pas, em Espanha e em terceiros mercados, ao mesmo tempo que facilita a penetrao das exportaes espanholas no mercado nacional e em mercados nossos clientes. +Os principais opositores do Presidente russo, Boris Ieltsin, vo reunir-se hoje em Moscovo para definirem uma estratgia que os leve ao poder. +Foi pelo menos essa a inteno do ex-vice-Presidente Aleksandr Rutskoi e do lder do Partido Comunista, Guennadi Ziuganov, quando convocaram o encontro que vai juntar, no centro parlamentar em Moscovo, a oposio nacional-comunista e as foras conservadoras que recusaram assinar, em Abril, o pacto de paz civil de Ieltsin. +A peseta continuou no topo do mecanismo de taxas de cmbio do SME onde a divisa portuguesa ocupa agora o terceiro lugar. +O Banco de Portugal no interveio no mercado interbancrio. +A taxa mdia do overnight desceu para 4,949 por cento. +No mercado da dvida pblica foram colocados 15 milhes de contos de Bilhetes do Tesouro a 182 dias, taxa mdia ponderada de 4,4593 por cento, contra 4,4887 conseguidos no ltimo leilo da mesma maturidade, realizado a 15 de Janeiro. +A criao da comisso de inqurito da AMS foi motivada por uma denncia de moradores de So Marcos, freguesia de Agualva-Cacm, de alteraes efectuadas nas cartas de ordenamento do PDM, aps a sua aprovao pelos deputados municipais, em que espaos antes urbanos passaram a zonas industriais. +Funcionrios da fiscalizao municipal foram tambm acusados de aconselharem residentes a venderem as suas propriedades. +Na proposta de relatrio elaborada pelo relator, Silvino Teixeira (CDU), constata-se que desde 1982, incio da elaborao do PDM, at Maro de 1993, a autarquia esteve totalmente alheada da gnese, definio, estudo e acompanhamento do plano. +Mas a situao no melhorou aps a aprovao pela AMS, em Abril de 1994, pois as linhas mestras do ordenamento do concelho no foram enviadas para ratificao governamental, como a presidente anunciara, mas tero ficado nas gavetas ou prateleiras da Cmara. +Mas para alm dos temas desse lbum, e em particular da verso minimalista de All apologies, um original de Kurt Cobain que Sinad adoptou j depois da morte do lder dos Nirvana, espera-se ainda a interpretao de algumas das outras canes que a tornaram notada, em particular as do lbum I Do Not Want What I Haven't Got (que continha Nothing Compares 2 U), j que as do trabalho que se seguiu, Am I Not Your Girl?, constitudo apenas por verses de velhos clssicos da msica popular acompanhados por orquestra, devero ficar de fora, por razes bvias. +Num pas com mais de sete mil ilhas e onde os golpes de Estado se foram sucedendo quase mensalmente nos anos 80 e onde a guerrilha fundamentalista islmica avana na zona meridional do arquiplago, os incidentes de ontem permitiram comisso de eleies dizer que o dia foi relativamente calmo. +Os cinco mortos e 26 feridos foram vtimas aparentes da guerrilha independentista muulmana, que atacou com fogo de morteiro vrias localidades nas ilhas de Mindanao, Sulu e Jolo, de maioria muulmana. +Nas eleies locais de 1988, ocorreram 149 mortos, e nas presidenciais de 1992 o balano foi de 63 mortos. +No Porto, a gare do aeroporto de Pedras Rubras era ontem pequena demais para albergar a confuso de gente que a inundou. +Havia bichas para tudo, desde a tabacaria at aos bares, onde as provises mostravam uma perigosa tendncia para se esgotar. +As pessoas viam-se obrigadas a improvisar lugares para esperar os avies que se iam atrasando. +Atrasos que, segundo as informaes recolhidas junto dos balces da TAP e da ANA, no se deviam greve dos pilotos, mas ao elevado volume de trfego e a um elemento que tem mais poder sobre os cus que os pilotos: o nevoeiro. +Os passageiros que se preparavam para partir, na sua esmagadora maioria, para frias no se mostravam muito preocupados com os atrasos que nalguns casos ultrapassavam as trs horas. +E quanto to falada questo de segurana, pelo menos Andreia Silva, de partida para Dublin, garantia que no havia meio de transporte mais seguro e aludia ao acidente da madrugada na portagem dos Carvalhos para exemplificar como viajar por terra mais perigoso do que pelo ar. +Mesmo o Gil, a mascote da EXPO 98, de braos abertos junto zona das partidas, parecia concordar com ela. +Sem os necessrios e competentes requisitos tcnicos e a adequada experincia para o exerccio daquele complexo e difcil cargo, dadas as suas exguas habilitaes, e sem um mnimo de conhecimentos de gesto, este mesmo senhor tem vindo a gerir a dita instituio de maneira desastrada, discricionria e inbil, transformando mesmo aquela jia do patrimnio duriense numa casa arruinada! + confrangedor olharmos hoje em dia para os meios de comunicao social e verificarmos o caos financeiro em que se encontra atolada aquela casa de to nobres tradies. +So notcias de penhoras, pelos tribunais, execues fiscais, montes de dvidas banca e Segurana Social, esbanjamento de vrios milhes em negcios pouco claros com a Real Companhia Velha, etc. etc. +Uma gesto calamitosa e incrivelmente ruinosa que os vinicultores durienses deveriam responsabilizar criminalmente! +Mas o que mais espantoso no meio de tudo isto que toda esta baguna provocada pelo senhor Mesquita Montes tem contado com a complacncia dos sucessivos governos, nomeadamente os seus vrios ministros e secretrios de Estado da tutela! +Tem contado tambm com o completo alheamento do Parlamento, que to pressuroso se mostra, por vezes, a discutir assuntos de lana caprina e nada faz para esclarecer este monstruoso escndalo! +E o mau exemplo dos Estados Unidos impede que a maior potncia militar possa herdar a tarefa de controlar os conflitos que persistem ou estejam a caminho. +A partir de agora, as guerras como a de Angola ou da Bsnia so praticamente impossveis de travar de fora e qualquer interveno -- cirrgica ou no, em nome da ONU ou no -- acarreta um grau de incerteza to grande que, ao ser accionada, no s pode eternizar os fogos que se pretendiam extinguir como ainda desencadear incndios em zonas dantes poupadas. +Foi j em tom de festa que a Comisso Central da Queima das Fitas de Coimbra anunciou o pr-programa da grande celebrao dos estudantes deste ano, que ter pela primeira vez uma mascote: um morcego de ar matreiro, que em sete poses diferentes retrata o estudante ao longo dos sete longos dias da Queima. +a Comisso Central continua a contar com a participao da Seco de Fado, como exige o regulamento, afirmando desconhecer oficialmente a deciso daquela de no participar no programa da Queima das Fitas/96. +Tudo por causa da atribuio a outro grupo acadmico, a Fan-Farra, de uma actividade que os Fados consideram sua: o Festival de Tunas (ver PBLICO de 23/2). +Nuno Guerra, presidente da Comisso Central, minimizou o diferendo entre as duas estruturas e afirmou que a linha orientadora trabalhar com a Seco de Fado, pelo que o convite vai ser feito, como hbito. +S se o Fado no aceitar que a Comisso assume a realizao dos dois eventos. +Problema ser conseguir fazer uma Serenata Monumental sem os fadistas da Academia, que entretanto prometem fazer uma serenata paralela, no mesmo dia e mesma hora, e em local onde os estudantes no podero ignorar. +No entanto, aps esta corrida ao dlar durante a primeira metade da semana, o mercado optou por uma posio mais defensiva que provocou um ligeiro recuo do dlar face generalidade das restantes divisas. +Os factores que contriburam fortemente para este movimento de correco foram, por um lado, os receios de intervenes por parte da Reserva Federal travando a rpida apreciao da paridade dlar / iene e, por outro lado, o facto de o Bundesbank ter deixado uma vez mais as taxas de desconto e lombarda inalteradas respectivamente nos 5.75 por cento . e 6.75 por cento. +LLoyd Bentsen, secretrio do Tesouro norte-americano, afirmou, num discurso proferido na quinta-feira, que um iene fraco no soluo aceitvel para os problemas econmicos no Japo e insistiu na necessidade urgente de um estmulo efectivo que atenue o enorme excedente comercial japons. +Foi com duras crticas ao presidente da Cmara de Vila Real, Manuel Martins, que o ex-nmero dois desta autarquia, Caseiro Marques (PSD), renunciou esta semana ao seu mandato. +Na sua opinio, Manuel Martins, que tambm lder da Concelhia do PSD local e que agora se apresenta novamente como candidato Cmara, afunilou o PSD a nvel local e no est altura de conduzir este partido para o futuro. +At concluso deste complicado processo, a CGD dever continuar a suspender a execuo das dvidas, excepto para as situaes de abandono. +Este acordo surge aps anos de presses polticas e sociais para resolver a situao irregular das 234 famlias que podiam ficar na rua, uma vez que a Caixa Geral de Depsitos tinha iniciado autos de penhora. +A Urbanizao da Bela Vista foi construda no incio dos anos 80, no mbito de um contrato de desenvolvimento de habitao celebrado entre o antigo Fundo de Fomento da Habitao (actual IGAPHE), a Caixa Geral de Depsitos e a empresa construtora Amadeu Gaudncio, tendo na altura a Cmara Municipal do Montijo favorecido a aquisio de um terreno e procedido iseno de taxas por ser um contrato de habitao social com custos limitados. +O processo de execuo fiscal vai passar a ser exclusivamente aplicado na cobrana de dvidas ao Estado e a outras pessoas de direito pblico, de acordo com o decreto-lei 241/93 de 8/7/93. +O fim desta norma evitar a execuo fiscal de dvidas de organismos sujeitos a um regime de gesto privada, como a Caixa Geral de Depsitos. +Mas no ser aplicvel aos processos pendentes. +Relativamente ao FEF do presente ano (um bolo global de 253 milhes de contos), as autarquias portuguesas devero, assim, receber no prximo ano mais quase 18 milhes de contos. +Um crescimento que, todavia, no deixa a ANMP satisfeita. +Lamentamos profundamente que as verbas para as autarquias voltem a ser atribudas em funo de uma lei desadequada e injusta, mas infelizmente os partidos no se entenderam quanto promulgao de uma lei que cumpra a Constituio da Repblica e que promova uma distribuio equitativa dos dinheiros do Estado, criticou Mrio de Almeida. +Pelo seu lado, Jos Augusto de Carvalho preferiu sublinhar que o aumento supera em muito o valor previsto para a inflao no prximo ano (dois por cento), acrescentando que o actual Governo tem cumprido a Lei das Finanas Locais em vigor. +A Associao Nacional de Municpios Portugueses pode ter outra leitura, mas esta realidade inequvoca, acrescentou. +Das vsperas da institucionalizao do Estado Novo queda da cadeira que afastou Salazar, meio milhar de cartas trocadas entre o ditador e o seu delfim, que ilumina zonas vitais de um longo e complexo relacionamento que foi feito de solidariedade e de conflitualidade, de intensidade e de distanciao, de sintonia e de divergncia. +Com prvio enquadramento histrico. +Nos pavilhes da FIL, os visitantes podero apreciar a variedade da oferta de embarcaes a motor -- a lancha Campion Marine, de 18 ps, por 3200 contos; a Status 180, um arrojado exemplar de 18 ps, com atrelado, a 4730 contos; ou a sofisticada linha italiana Cranchi, especialmente a de 36 ps e a Azimute, o mais luxuoso fly bridge do mundo, que custa 50 mil contos. +Entre os extremos, v-se tambm uma coleco de embarcaes da tradicional Bayliner, a refinada Caravelle, Celebrity, Searay, Argus, Chaparral, Rinker, Fairline, Gobbi, Sealine, Nimbus e Cobalt, a ltima considerada o Rolls Royce dos mares. +Como atraces extra, apresenta-se o Balt, um pequeno barco de 5,30 metros, com motor de 50 cavalos, vocacionado para o uso em rios, por 3679 contos, e a Cobia, uma linha de barcos para pesca com consola central. +Nos veleiros, as opes no so tantas, sendo a principal novidade representada pelo Jeanneau 24, de 24 ps, que se pretende organizar em classe nacional para futuros campeonatos, alm dos populares e prticos veleiros McGregor e dos conhecidos GibSea, barcos de cruzeiro seguros e confortveis. +Em termos de navegao, h uma oferta vasta e actual de equipamentos de convs e electrnicos, especialmente sondas (com um novo modelo que permite a visualizao do fundo, alm da proa, o Echopilot Incastec) e GPS (Global Posiotioning System). +Desde o Vero de 1993 mais de sete mil xiitas iraquianos fugiram para o Iro, atravessando imensos lamaais, para escapar s perseguies e ataques do Exrcito do presidente Saddam Hussein. +Calcula-se que mais de 20 mil pessoas tenham abandonado as suas vilas e aldeias e se dirijam para reas prximas da fronteira. +J em territrio iraquiano contam -- em relatos que tm surgido nos ltimos meses, sobretudo na imprensa britnica -- o pesadelo que deixaram para trs: quilmetros e quilmetros de terrenos lamacentos onde anteriormente existiam pntanos, rios, lagos, culturas agrcolas; aldeias destrudas, populaes massacradas, animais e plantas mortos em consequncia do que se suspeita serem ataques com armas qumicas. +As bases de dados piratas existentes em Portugal dedicam-se afixao e difuso de cpias ilegais de programas sujeitos a proteco, de resultados de experincias de penetrao em sistemas informticos, de informao acerca de proteces, de portas de acesso e de passwords de entrada nas redes Telenet, Telepac, Itapac e, em alguns casos, nmeros de cartes de crdito. +Algumas das chamadas portas de entrada vendidas eram da Lusa, do Laboratrio Nacional de Engenharia Civil (LNEC) e da empresa Aeroportos e Navegao Area (ANA). +A heterogeneidade dos alunos que nos dias de hoje chegam escola determina diferentes atitudes e, obviamente, diferentes tipos de indisciplinas. +Durante o debate foram claramente distinguidos dois deles: aquele que tpico do ensino preparatrio e das classes baixas e aquele que se verifica mais no secundrio e que protagonizado em conjunto por jovens das classes mdias-altas, que se sentem protegidos pela sua origem. +Momentos antes deste verificao, Albino Esteves, professor da Secundria de Clara de Resende, enunciava medidas preventivas para a indisciplina na escola: o olhar reprovador do professor, o silncio tctico, a sobrevalorizao dos pequenos sucessos dos alunos mais fracos e o acompanhamento discreto das crises dos alunos. +De um memorando de Jorge Ferreira sobre a eleio anual dos lderes dos grupos parlamentares. +Poder dar alguns exemplos concretos do programa que tinha apresentado? +No foi verdade, por exemplo, que os prprios livreiros se manifestaram contra a realizao de concertos e debates, considerando que isso era desmobilizador dos potenciais compradores? +Isso no pode ser dito fora de contexto. +fizemos um palco suspenso e as pessoas que convidmos tambm eram um chamariz para o imenso pblico que acorreu ao local. +Protestaram, com alguma razo, porque os concertos foram previstos tardiamente, visto que as eleies para a APEL foram muito em cima da data da feira. +Mas as pessoas acabaram por admitir que tnhamos feito o nosso melhor. +Numa reunio no final da feira, pedimos desculpa pelo ocorrido e fomos perdoados e ovacionados, porque foi considerada uma iniciativa interessante. +Na altura, assumi o compromisso de este ano ser dialogante com as pessoas; de preparar a feira com tempo, de tomar nota de um conjunto de crticas. +Mas pensava que esse modelo seria de manter em 1996. +Por exemplo, uma das minhas propostas inclua um concerto com Mrio Laginha e Pedro Burmester, e tambm a vinda de Eunice Muoz ... +O Comissrio europeu para o audiovisual apelou anteontem a uma mudana de atitude da indstria europeia do sector, de modo a adaptar-se globalizao mundial das redes de distribuio. +A preparar um Livro Verde sobre a poltica europeia para o sector, Joo de Deus Pinheiro, falando num seminrio sobre os media, considerou que a Unio Europeia devia encorajar o desenvolvimento de novas e mais eficazes redes de comunicao transeuropeias. +A TVI-Televiso Independente e a TDF-Telediffusion de France fundaram uma sociedade de teledifuso para operarem em Portugal, a RETI, que ter sede social em Lisboa. +O acordo foi assinado no incio do ms e, na nova empresa, a TVI detm 55% do capital e a TDF 45%, podendo o capital vir a ter mais participaes portuguesas. +Est prevista a cobertura de 80% do territrio portugus, atravs de 19 estaes repetidoras. +As associaes servem para prestar servios sociedade empresarial e aos fins dos empresrios. +Mas, se me pergunta se o parque de feiras de Lisboa ser mais rentvel que o do Porto, eu pessoalmente digo-lhe que estou convencido que sim. +Desde logo por causa do grande afluxo de pblico ... +O nome diz tudo. +O nome do disco, e o nome do grupo que o assina. +A coleco completa dos singles dos Pet Shop Boys um daqueles registos que o consumidor vido de msica pop no pode deixar de receber de braos abertos. +Imaginem s :18 canes de puro sumo pop, sem bafio nem conservantes. +Se o formato sinttico -- tanto na frmula da cano utilizada pelo grupo como pela sonoridade que a envolve --, neste caso o palavro s tem conotaes positivas. +Porque Neil Tennant e Chris Lowe tm, de facto, uma capacidade extraordinria para surpreender e agradar. +Porque sabem ser concisos, sem falsas simplicidades, porque so inteligentes, sem armar grandes espalhafatos, porque conseguem ironizar e autoparodiar-se sem fazer figuras tristes. +A luta de todos, o que prejudica os colegas, prejudica-me a mim, disse Maria Teresa Feio, educadora de infncia de Albergaria, enquanto se preparava para participar na manifestao organizada pela Federao Nacional de Professores (Fenprof), ontem, em Lisboa. +Acabou a America's Cup de 1992, viva a de 1995. +Os americanos conseguiram defender a posse do trofu, mas os desafiantes prometem regressar mais fortes, daqui por trs anos, baa de San Diego. +os cubanos podem simplesmente no acreditar que os Estados Unidos os retenham perpetuamente em Guantanamo, ou que os enviem para outros pases, e ento a fuga continuar. +O estmulo imediato para a mudana de poltica foi o receio de que a Florida fosse invadida por uma vaga de refugiados -- uma possibilidade que traria srios perigos polticos. +a incapacidade de Jimmy Carter em conter a vaga de embarcaes vindas de Mariel em 1980 contribuiu para a sua derrota a favor de Ronald Reagan. +Clinton, cuja poltica externa tem uma pobre reputao, ficaria seguramente mais vulnervel s crticas. + claro que nesta perspectiva as autoridades espanholas tudo tentaro fazer para integrar o peloto da frente, cenrio sem dvida mais favorvel a Portugal. +Mas as coisas em Espanha no esto nada fceis. +A derrapagem da economia acentua-se. +um em cada cinco espanhis esto sem emprego. +O Governo socialista previa que essa fasquia fosse atingida apenas no final deste ano, depois de um forte aperto na economia, mas, supreendentemente, foi j alcanado. +Com este estado de coisas, iniciou-se uma forte presso dos mercados sobre a peseta. +O Banco de Espanha foi chamado a intervir e comearem os rumores de que a peseta poderia tomar o caminho da libra e da lira e abandonar o SME. +Calcula-se o que poderia posteriormente suceder ao escudo, sabendo-se, como se sabe, que os tcnicos do Comit Monetrio aconselham uma desvalorizao substancial da moeda nacional. +Como em Frana o clima tambm de incertezas, agravado pelas eleies marcadas para meados do prximo ms, a Europa passa nas prximas semanas por um perodo periclitante. +To periclitante, que se sobrevier nova tempestade cambial, o SME muito simplesmente poder acabar e dar o lugar tal UEM acelerada para as economias mais fortes, e mais lenta para os que ainda tm muito que convergir. +cada unidade uma unidade. +O processo do desejo e da sua impossvel misso. +Ao entregar um cargo equiparado a ministrio representante de um partido historicamente avesso aos compromissos polticos, Itamar Franco abriu uma crise sem precedentes no Partido dos Trabalhadores (PT). +Eu curvei-me s razes do Presidente, disse Luiza Erundina, que contrariou o veto pblico de Lula participao do PT no Governo e o prprio partido, que j havia declarado oficialmente a sua oposio a Itamar Franco. +Eleita para suceder ao ex-presidente Jnio Quadros na Prefeitura de So Paulo em 1988, Luiza Erundina, de 58 anos, geriu at ao final do ano passado o terceiro maior oramento do Brasil. +Fui oposio a vida toda. +s vezes cheguei a ser radical e sectria, mas aprendi que a gente s interfere se estiver dentro do barco, declarou a ex-prefeita imprensa paulista, ao justificar a sua presente deciso. +O mundo est a tornar-se amigo das mulheres, mas as salas dos conselhos de administrao no. +Menos de trs por cento das empresas ocidentais so dirigidas por mulheres, revela um estudo da Organizao Internacional do Trabalho (OIT), ontem divulgado em Genebra. +O reprter da Rdio Elmo de Pinhel, Craveiro Lopes, foi condenado em tribunal a 14 meses de priso e 60 dias de multa por ter criticado a actuao da PSP, informou ontem a Lusa. +A pena foi suspensa por 28 meses, atendendo conduta do arguido anteriormente aos factos e por ter demonstrado algum arrependimento por os haver praticado. +O reprter foi accionado por, em crnicas emitidas em Julho de 1992, ter criticado o comandante do posto da PSP, subchefe Amadeu Eiras e o guarda Antnio Soares, crticas que o Ministrio Pblico considerou difamatrias. +De acordo com o processo, Craveiro Lopes fora detido, meses antes, por aqueles polcias por factos susceptveis de integrarem ilcito criminal, aps o que ter dito aos microfones ser inconcebvel que, nas mos de homens sem qualquer escrpulo, se deposite poder. +Em tribunal, o reprter afirmou no ter tido inteno de difamar, mas to s de informar, no pretendendo atingir a honra e considerao dos visados. +O tribunal considerou, porm, que as afirmaes de Craveiro Lopes so objectiva e subjectivamente difamatrias e susceptveis de atingir seno a essncia da personalidade destes, pelo menos o patrimnio do seu bom nome, do crdito e da confiana por eles adquiridos enquanto pessoas, cidados e agentes da autoridade numa comunidade pequena onde todos se conhecem. +Em declaraes Lusa, Craveiro Lopes disse que se limitou a tornar pblicas situaes de perseguio que ainda perduram. +Se hoje tiver de fazer e dizer o que disse, no hesitarei. +O presidente da Associao Portuguesa de Surdos advertiu ontem que o desenvolvimento da maioria da populao surda deixa muito a desejar. +Falando Lusa do I Congresso Nacional de Surdos, que decorrer durante o fim de semana em Coimbra, Jos Bettencourt considerou que os mtodos de ensino tm feito tudo ao contrrio e os resultados esto vista. +Chamou a ateno para o facto de sendo a lngua gestual pura e rica do ponto de vista gramatical e lingustico, e constantemente ignorada pela sociedade, importa alertar para a sua importncia na educao, formao e na plena integrao na vida activa. +Sem rejeitar o mtodo oralista, Jos Bettencourt defende que o sistema educacional da criana surda deve contemplar lngua gestual, escrita e falada. +A isto, precisou, damos o nome de comunicao total. +A questo dos televisores estereofnicos tem agora alguma actualidade, uns vez que muitos dos consumidores que compraram aparelhos com essas caractersticas antes do incio das emisses em estreo so agora confrontados com o facto de os seus televisores no emitirem em estreo o sinal que recebem das emissoras. +Vindo directamente do Algarve, de avio, chegou s instalaes da Igreja ao volante de um Mercedes e vestindo uma t-shirt. +Vestiu depois um fato azul escuro, com gravata vermelha e camisa branca de riscas cinzentas. +E culos escuros -- porque estava muito cansado -- que tirou para o fotgrafo. +J se definiu uma vez como moo de recados de Deus. +Qual foi o primeiro recado que recebeu? +Dentro dos filmes (curtas e mdias metragens de 17 pases) sobre temticas actuais destaca-se a presena de Black Harvest de Bob Connoly (Austrlia), Aspen de Frederick Wiseman (EUA), Face Value de Johan Van Der Keuken (Holanda) e Das Ungehobelte Pack de Nana Swiczinsky (ustria). +A retrospectiva / concurso de cinema documental asitico inclui ainda a homenagem ao cineasta sueco Stefan Jarl, qual a Cinemateca Portuguesa adere apresentando duas pelculas suas. +Outras quatro obras de Jarl estaro presentes no festival, simbolizando a metdica abordagem de um dos cineastas suecos mais experimentais, mais radicais, traando sempre novas vias no documentalismo, como salienta Manuel Costa e Silva. +Tambm Portugal se encontra representado com a estreia mundial de Crenas e Lendas de Joo Soares Tavares, obra inscrita na retrospectiva RTP, homenageada nestes III Encontros. +Nas dcadas de 60 e 70, a produo documentarista em Portugal tinha um ritmo diferente e raizes de produo diversas, devido estrutura das sesses de cinema que permitia o visionamento de um bloco de filmes curtos e de noticirios antes da apresentao das obras ficcionais do programa. +Assim, as empresas apostavam em pequenos filmes de prestgio, longe dos esforos de canalizao de publicidade que a televiso hoje mobiliza, para serem projectados nas salas de cinema e esses documentrios passaram a ser, para essa gerao, a grande escola inicitica para quem desejava seguir a aventura do cinema, relembra Manuel Costa e Silva. +A fase II do terminal multipurpose, o novo porto de pesca com a respectiva lota, o sistema de comando e controlo dos terminais petrolfero e petroqumico, diversos acessos rodo-ferrovirios e a beneficiao e tratamento paisagsticos da zona marginal de Sines, investimentos rondando os 4,7 milhes de contos, foram as infra-estruturas que Azevedo Soares inaugurou. +O ministro -- que referiu representarem as receitas do porto de Sines 12 por cento do produto interno bruto do Alentejo, assegurando cerca de 4200 postos de trabalho -- elogiou a boa colaborao entre a autarquia local, a Cmara de Sines, e a Administrao do Porto de Sines, APS, afirmando que em Sines disse-se pouco e fez-se muito, numa indirecta aos problemas entre a Administrao do Porto de Lisboa e a cmara da capital. +Porque que isto aconteceu? +No porque todos os condutores fiquem subitamente atacados de uma febre Fittipaldi quando se sentem como que rodeados por algodo doce. +O que acontece, explica Robert Snowden, que estuda a percepo visual do movimento h mais de dez anos, que quando est nevoeiro vemos o mundo com menos contraste. + Quanto maior o contraste detectado pelos nossos olhos, mais rpido as coisas parecem andar, diz o investigador. +Isto porque os nossos olhos confundem facilmente uma mudana de velocidade com uma alterao do contraste. + que as clulas localizadas numa zona do crebro chamada rea visual 5 so to sensveis a uma como ao outro e a linha de fronteira entre estas duas situaes tnue. +As pessoas que tm esta rea do crebro danificada no so capazes de ver o mundo em movimento. +Para elas, est tudo sempre parado, numa imagem muito difusa, explicou Snowden. +-- de toda a maneira, no aceito, enquanto coordenador, pela Antena 1, dos referidos programas, as suas afirmaes de academismo, montagem que deixa muito a desejar, soporfero, e a referncia s bibliotecas como espao a evitar por quem trabalha em rdio -- referncia, no mnimo, curiosa. +Com participao de elementos do grupo, neste caso T Pereira, ser lanado pela Tribal um CD s com trabalhos da Kaos (os temas includos devero repartir-se entre os primeiros lanamentos e os mais recentes) misturados numa megamix de T Pereira. +Ainda deste, aqui sob o nome de DJ Vibe, ir ser lanado j em Janeiro um maxi-single j anunciado pela Kaos, a que se juntaro os dos Urban Dreams e T Ricciardi. +Depois disso, a Kaos tem prevista a estreia do LL Project, do DJ Lus Leite, de um novo projecto do Porto designado Algo Rtmico, um novo maxi dos Ozone, e lbuns destes dois ltimos projectos. +Em finais de Fevereiro, princpios de Maro, ser editada uma segunda compilao da Kaos que vai incluir todos os lanamentos no contidos em Totally Kaos, mais trs edies exteriores editora -- um projecto chamado Duplex de um portugus radicado na Alemanha, e mais dois em negociao -- e ainda uma remix pelos USL para Bottom heavy de Danny Tegnalia. +A explorao do aparthotel, que ter 134 quartos -- todos eles com kitchenete e distribudos ao longo de oito pisos (entre o segundo e o nono) -- caber cadeia Orion, pertencente aos scios franceses do grupo Amorim, maioritrios na Inogi. +J a explorao das reas comerciais, que se vo distribuir por trs pisos -- o rs-do-cho, o primeiro piso e um mezzanino -- est ainda por definir a quem ir ser atribuda. +Estamos em negociaes com vrias empresas, entre elas a FNAC, mas no h ainda negcio fechado com nenhuma delas, disse ao PBLICO Almeida Guerra. +O Decreto Regulamentar 19/91 (B) descobriu que a factura e a guia de remessa permitem controlar o efectivo carcter, pblico ou particular, do transporte, pelo que acabou com a guia de transporte. +Menos papelada sempre bom, ainda que saiba a pouco. +Se quiser telefonar -- servio manual -- para o Transkei prepare-se para pagar 51000 por minuto, o mesmo que, por exemplo, para o Afeganisto, Qatar, Gmbia, Brunei ou Honk-Kong. +J se quiser falar, ainda por servio manual, para Trindade e Tobago, Tortola, Estados Unidos, Jamaica ou Austrlia, o custo por minuto ser de 34000. +Um Hrcules C 130 da Fora Area Portuguesa seguiu ontem para Moambique e poder vir a servir, no sbado ou no domingo, para o transporte de So Tom para Lisboa de mais um grupo de estrangeiros que entretanto sejam retirados do Huambo. +Na sua viagem para Maputo, o avio militar portugus transportou 27 militares moambicanos, dois cabo-verdianos e seis so-tomenses, que participaram em aces de formao em Portugal, para alm de algum material destinado aos portugueses do batalho de comunicaes (BT4) que integra a misso das Naes Unidas em Moambique (Onumoz). +O gabinete do primeiro-ministro vai apresentar queixa na Alta Autoridade para a Comunicao Social contra o jornal Semanrio, que ontem titulou em primeira pgina que Cavaco deu 2,5 milhes de contos ao PSD. +De acordo com a nota oficiosa emitida pelo gabinete de Cavaco Silva, o referido ttulo completamente falso e abusivo porque o primeiro-ministro no d dinheiro aos partidos, e associa o teor da notcia a um procedimento legal luz da lei de financiamento dos partidos. +Em termos de unidades vendidas, se se venderam cerca de 145 mil LP de preo mximo, em CD, na mesma escala de preo, venderam-se quase 405 mil -- uma discrepncia que se acentua na facturao (176 mil contos em LP de preo top contra cerca de 817 mil contos em CD tambm de top), devido ao segundo formato ser vendido sensivelmente pelo dobro do primeiro. +A cassete do mesmo escalo continua a no ir muito bem. +Vendendo menos que o vinil: pouco mais de 103 mil, o que corresponde a uma facturao de cerca de 124 mil contos. +Os discos mais vendidos so os que chegaram ao fim do ano passado j com maior nmero de galardes. + o caso por excelncia de Waking Up The Neighbours, de Bryan Adams, que agora chegou a sextuplo de platina (cada disco de platina equivale venda de 40 mil unidades). +As colectneas de xitos dos Bee Gees, Queen e Tina Turner tambm recolheram mais um galardo de platina. +O nico grupo portugus que alcanou este estatuto foram os Onda Choc com Ela S Quer, S Pensa Em Namorar. +O lbum de estreia dos Resistncia tambm j disco de platina, mas ainda no consta nas contas do trimestre, porque s o alcanou em Abril. +P. -- Face aos dados de que dispe, continua a acreditar em poder alcanar a maioria absoluta? +R. -- Continuo. +P. -- J percebemos a sua cautela. +Define a maioria absoluta como um objectivo, mas, se no a atingir, isso tambm no ser para si uma derrota ... +O Comit Central do PCP est reunido para analisar a situao na Unio Sovitica. +Em cima da mesa, ainda a situao interna do partido. +Mas os dirigentes iro tambm poder ler um documento sado de uma reunio em que o indito aconteceu. +Militantes comunistas encontraram-se em pblico e exigiram a antecipao do XIV Congresso. +Querem novos dirigentes, nova ideologia, novo programa, novos estatutos. +Em suma, um novo partido. +Esto dispostos a lutar por isso, mas vo fazer uma pausa at 6 de Outubro. +Com uma carteira de ttulos obrigatria para o estudo da obra nemesiana -- Vitorino Nemsio.A Obra e o Homem, ed. Arcdia, 1978, Temas Nemesianos, ed. Angra, 1981, e Vitorino Nemsio -- Luz do Verbo, ed. Vega, 1988, alm de ter prefaciado vrios livros para a Imprensa Nacional-Casa da Moeda, que est a publicar as Obras Completas --, Martins Garcia encontrou-s pela primeira vez com Vitorino Nemsio num exame. +Foi em 1960, na Faculdade de Letras de Lisboa. +Actualmente a leccionar Teoria da Literatura e Literatura Portuguesa na Universidade dos Aores (doutorou-se, em 1985, com a tese Fernando Pessoa: ' Corao Despedaado ' ), tem sido -- a par de David Mouro-Ferreira, Antnio Manuel Machado Pires, Ftima Morna e Maria Margarida Maia Gouveia -- um dos investigadores que mais tem contribudo para que Nemsio no caia no esquecimento. +Contactado pelo PBLICO, o mdico confirma ter entregue os medicamentos a um senhor que se calhar facilitou e despejou [ o lixo ] de qualquer maneira. +Esta , para David Paiva, uma justificao suficiente para no se considerar directamente responsvel pela situao criada. +Mesmo depois de ter sido informado que, ao incio da tarde de ontem, o PBLICO observou trs crianas -- o Antnio, de 8 anos, o Tiago, de 12 anos e o Lus, de 10 anos --, todos residentes no bairro camarrio da Pasteleira a brincar no meio do lixo. +Estvamos s a mexer nas latas. +No tocamos nos remdios afirmou Lus. +Mas o Bruno, de 10 anos, e tambm residente no Bairro da Pasteleira, revelou que o lixo tinha l umas seringas que depois pusemos a arder. +Apesar da evidncia do perigo, David Paiva, limita-se a notar que no se tratam de lixos do tipo hospitalar. +So coisas secas que j esto fora da validade. +Mas para o responsvel pelos Servios de Fiscalizao da Cmara do Porto, Jos Adriano, os produtos encontrados representam sempre perigo, mesmo que no estejam no prazo de validade. +So coisas que j esto de tal maneira fora de validade que j no tm aco farmacolgica. +Ou seja, podem no cumprir o fim para o qual so indicadas mas no deixam, por isso, de constituir perigo se forem ingeridos. +Penso que no porque o tempo tambm desactiva os medicamentos, remata, sem certezas, o mdico. +Lus Afonso -- Quando comecei a fazer o Bartoon, em 1993, tinha essa angstia. +Mais do que isso, vivia aterrorizado com o facto de ter de arranjar uma ideia todos os dias. +Hoje vivo em paz com esta relao. +P. -- Criaste condies para isso, j que vives em Serpa. +Rodeaste-te de algum material especial, como foi? +O cineasta americano Steven Spielberg apresentou na quinta-feira, em estreia mundial, num ptio de Liceu Sophie Charlotte, em Berlim, na presena de uma plateia de jovens alunos impressionados e atentos, um documentrio bastante emotivo sobre os sobreviventes do holocausto, um CD-Rom, intitulado O Futuro da Educao. + uma maravilha estar aqui convosco disse o autor da Lista de Schindler, que foi agraciado com a Grande Cruz de Mrito, a mais alta distino alem. +Porque o debate foi um pantanal de lugares-comuns e de manobras de diverso, no qual os oradores, em vez de encararem objectivamente a realidade dos factos, procuraram, antes de mais, a minimizao do papel dos outros. +At o moderador do debate contribuiu para o arraial, dando ares de perdio que sugeriam a imagem dos negociadores comunitrios face impossibilidade de um cessar-fogo na Bsnia. +E a quota do tomate, perguntou, a dada altura do caos em que se transformou a discusso sobre uma reforma que apenas contempla os sectores das culturas arvenses, do leite, da carne bovina e do tabaco. +mostrou que, apesar das boas palavras, a capitalizao de protagonismo poltico est acima da discusso sria e profcua. +e o homem da CNA, repetiu a confrangedora ideia que o documento inicial da reforma protegia as exploraes extensivas e que o documento final as penalizava, quando, afinal, poucas linhas se alteraram nesta questo. +A conquista de mercados parece ser, no entanto, a preocupao imediata da Soporcel para os prximos tempos. +At data, a rede de distribuio da Wiggins Teape demonstrou que a nossa opo estratgica tem sido a mais correcta, considerou lvaro Barreto, realando a necessidade de um parceiro externo que opere nos mercados mundiais do papel. + Alm disso, a Soporcel est em vias de adquirir uma importante posio numa distribuidora espanhola, divulgou o presidente do Conselho de Administrao da Soporcel. +Os mercados da Europa do Sul so para j os alvos estratgicos para a distribuio do papel produzido pela empresa, principalmente os dos pases Ibricos que so os que mostram mais elevada taxa de crescimento, cerca de 4,5 por cento ao ano. +Os produtos fabricados so, fundamentalmente, o papel de cpia, o papel de impresso offset e os papis de computador, que representam os mercados mais promissores. +Lusa Senos, chefe de Diviso de Sismologia do Instituto de Meteorologia (IM), relatou os ltimos progressos da rede nacional de sismgrafos. +Gerida pelo IM, a rede funcionava desde os anos 70 com nove estaes analgicas em Portugal continental, 11 nos Aores, uma na Madeira e outra em Macau. +Em cerca de 600 sismos no continente, s em dez a 12 por cento se conseguia calcular os parmetros ssmicos. +Razo que levou o IM a decidir a instalao de duas redes digitais. +Uma das redes digitais, em fase de instalao entre 1994 e 1997, custar 150 mil contos, ter 14 estaes (12 no continente e duas na Madeira). +Neste momento, conta j com seis estaes no continente. +Os registos falam por si, pois as estaes analgicas no detectavam sismos de magnitude inferior a trs na escala de Richter. +Em 1995, duplicou o nmero de sismos detectados. +Com a rede digital detectaram-se sismos de magnitude inferior a dois. +A segunda rede digital, que ser instalada nos Aores entre 1997 e 99, ter 12 estaes e custar cerca de 200 mil contos. +O Trifsica um bar da 24 de Julho, isto , longe das minuscularias do Bairro Alto. + muito engraado. +As portas e janelas so no nmero das letras do bar, ostentando, uma a uma, cada letra da palavra T-R-I-F--S-I-C-A, em torno do gaveto das Escadinhas da Praia com a 24 de Julho, num vidro fosco, em que s transparece o corpo da letra. +Nove corpinhos bem feitos. +A decorao tambm divertida. +Detrs do balco, as bebidas protegidas pela barreira visvel de um vidro esto ligadas a mangueiras, que entornam os preciosos lquidos computadorizadamente para os nossos copos. +Isto equivale a no poder servir melhor o whisky de um amigo, a no ser que lhe mangueire dois para dentro do copo. +E a tem de se haver com o computador, que lhe cobra os dois! +Enfim, este convvio com computadores custa-nos um bocado. + que so uns antipticos que nunca oferecem copos. +A no ser que os homens, seus donos, lhes ordenem! +Boa! +Mas, para alm de no nos oferecer copos, a no ser que a obriguem, esta mquina muito bonita. + to gira que a empresa que a comercializa se chama Coisas Giras. +R. -- Vamos l a ver. +nos primeiros tempos, a minha viso era a de um homem que vivia numa boa casa, com ar condicionado, boa comida, comodidades. +Nesses tempos at olhvamos com alguma sobranceria para as populaes, eu ainda no me tinha apercebido de que os negros talvez no fossem afinal uns selvagens, tinham era uma cultura diferente ... +Depois, foi na guerra, quando tive de penetrar no mato e andar pelas bolanhas da Guin ou pelas savanas de Angola, quando descobri povoaes isoladas, etc., que passei de facto a conhecer frica. +P. -- Onde estava quando comearam os massacres em Angola? +A anomalia criou dificuldades, nomeadamente, em fbricas e restaurantes locais. +Tambm, ontem, em entrevista RDP-Madeira, Alberto Joo Jardim, presidente do governo regional madeirense, surgiu em defesa de Jorge Sampaio, numa autntica trgua, face aproximao da presidncia aberta de Sampaio no arquiplago. +Mostrando dvidas quanto interpretao da lei que revogou a obrigatoriedade do PR ouvir o CEMGFA na reconduo de um chefe de ramo, Sampaio, disse o lder do PSD-Madeira, ter agido bem, j que, sublinhou, uma carta daquelas no se escreve ao Presidente da Repblica. +At porque Fuzeta da Ponte, nas palavras de Alberto Joo, no era grande espingarda e antiptico, devendo por isso ter sido demitido logo. +Depois, Jardim aproximou-se da tese defendida pelo PCP, da exigncia da demisso de Veiga Simo, ao defender que o actual ministro da Defesa, com Marcelo Caetano j dera cabo da Educao. + TEATRO NACIONAL DE D. MARIA II. +Hoje, s 21h30; amanh, s 16h00 e s 21h30; dom., s 16h00; 2, s 21h30. +TEATRO NACIONAL. +Dias 5 e 7, s 18h00. +A Gr-Bretanha estar preparada para impor o seu domnio directo sobre Gibraltar, para controlar a banca e o sistema legal respectivos, numa tentativa para acabar com as alegaes de que a colnia se transformou num centro de lavagem de dinheiro, informou ontem o jornal Sunday Telegraph. +Funcionrios do Governo britnico confirmaram que a Gr-Bretanha foi aos arames com as posies das autoridades gibraltinas sobre a concretizao de directivas da Unio Europeia, bem como sobre o tema da lavagem de dinheiro. +O jornal acrescenta que os gibraltinos receiam que a medida seja um primeiro passo para os britnicos desistirem da soberania sobre o rochedo, situado num promontrio no sul de Espanha, devolvendo-o aos espanhis. +O jornal garante, embora um porta-voz oficial do Ministrio dos Negcios Estrangeiros no o tenha confirmado, que o Governo britnico est pronto para, antes do Vero, tomar medidas para que Gibraltar deixe o seu estatuto de colnia com governo prprio, passando a depender directamente da Coroa britnica. +Um mido de trs anos cujas pernas foram cortadas por uma ceifeira-debulhadora foi operado para a reimplantao dos dois membros e recupera no Hospital Rei Eduardo VIII em Durban, frica do Sul, informou ontem fonte hospitalar. +Amos Mosea brincava no meio de um milheiral, sexta-feira, numa quinta perto de Underberg, a 200 quilmetros da cidade porturia de Durban, no Oceano ndico, quando foi atropelado pela ceifeira-debulhadora e lhe cortou as pernas abaixo dos joelhos. +Os gritos da criana alertaram um vizinho que a conduziu a uma clnica local, em estado de coma e sangrando abundantemente. +Mas o mido saiu de coma para dizer: Estou doente. +Conduzido de helicptero para Durban com os cotos das pernas mergulhados em gelo, foi imediatamente operado durante oito horas. +Pouco a pouco, foi-se esquecendo em grande parte o objectivo primordial que para ali levara norte-americanos, paquistaneses, italianos e outros soldados de meio mundo: distribuir comida a muitos africanos que estavam perigosamente beira da morte. +E a situao passou a ser, em primeiro lugar, a de um confronto cada vez mais agudo entre a fora expedicionria estrangeira e as milcias locais, muito em particular a de Aidid, que se tem feito passar por um nacionalista e um paladino da cultura islmica. +O ataque areo de ontem, em que participaram nove helicpteros, e a operao terrestre que se lhe seguiu fizeram com que somalis furiosos tivessem morto um fotgrafo da agncia Reuter, o anglo-americano Dan Eldon, de 22 anos, e outro da Associated Press, o alemo Hansi Krauss, de 30, estando ainda por confirmar a morte de mais um fotgrafo, Hos Maina, e de um operador de som da Reuters Television, Anthony Macharia, ambos quenianos. +Para medir a largura da regio emissora da radiao, os astrnomos utilizaram, como se se tratasse de uma autntica lente de aumentar, a bolha de gs em expanso que envolve o pulsar. +O gs tambm constitui um dos restos da supernova e encontra-se em expanso sob o efeito da onda de choque gerada pela exploso. +Visto que o pulsar era demasiado pequeno para a sua imagem ser medida desde a Terra, mesmo pelos telescpios mais potentes, a ideia consistia em captar imagens do gs para, a partir da, extrair a imagem do pulsar. + A bolha de gs fornece uma resoluo equivalente de uma ' lente ' do tamanho da rbita da Terra, explica um comunicado ontem emitido pela Associao Astronmica Americana. +Para o presidente da CML, o objectivo da UCCLA no ter dinheiro a prazo nos bancos, mas servir as populaes das cidades que so membros. +E disse tambm que a transformao da UCCLA em fundao, proposta por Pinto Machado, uma novidade absoluta para todos os membros da Assembleia Geral. +O autarca classificou as suas relaes pessoais com Pinto Machado como sendo as melhores e considerou no haver qualquer ruptura no plano institucional, porque o secretrio-geral acaba de apresentar a sua demisso. +Negou ainda que as suas divergncias com Pinto Machado resultem de um conflito poltico-partidrio no municpio lisboeta (onde o PP est na oposio) que tenha sido transposto para a UCCLA, o que foi confirmado pelo secretrio-geral demissionrio. +Os desafios da globalizao e as respostas para o desenvolvimento de frica na viragem do sculo so o tema de um frum de dois dias, a comear amanh, em Lisboa. +Organizado pela Sociedade de Geografia de Lisboa, em colaborao com o ISCSP-Instituto Superior de Cincias Sociais e Polticas, o encontro conta com a participao de representantes das empresas portuguesas com investimentos em frica e dos respectivos beneficirios. +A Cmara de Palmela vai submeter apreciao da Assembleia Municipal uma proposta que reduz em 0,1 por cento a contribuio autrquica, fazendo com que, em 1995, esta taxa, em vez de representar 1,3 por cento sobre o valor dos prdios urbanos, passe a ser de 1,2 por cento. +Outra declarao, desta vez vinda do Governo indiano, voltou a deixar preocupadas as cinco potncias nucleares (EUA, Frana, Rssia, China e Gr-Bretanha). +Um responsvel indiano pelo projecto de fabrico de msseis, Abdul Kalam, disse que o Agni (Fogo) -- um engenho com capacidade para transportar uma carga nuclear a uma distncia de 1500 metros -- est pronto a ser produzido em srie. +E outro, com maior alcance, vir a caminho. +Segundo o assessor cientfico do primeiro-ministro indiano, Atal Behari Valpayee, trata-se de um Agni melhorado, com um alcance de 2500 metros, num estado avanado de desenvolvimento e cuja produo j recebeu a aprovao do Governo. +As sanes no nos afectaro neste domnio, disse ainda Kalam, referindo-se s penalizaes econmicas impostas contra a ndia pelos EUA, Japo e Canad. +O nosso programa nuclear 100 por cento indiano. +Um incndio de grandes propores deflagrou ontem, cerca das 20h00, na escarpa da Serra do Pilar, em Vila Nova de Gaia, numa fbrica de estatuetas localizada na margem esquerda do rio Douro. +Consciente da debilidade partidria no relacionamento com a sociedade, Gonzlez apostou forte em independentes de prestgio. +O efeito Garzn, a incluso na lista de Madrid do magistrado que mais reconciliou os espanhis com a Justia, no teve, no entanto, o efeito duradouro que o lder socialista pretendia. +To-pouco o tiveram as promessas de um novo impulso democrtico, necessariamente regenerador, uma das chaves da sua campanha, porque no foram completadas com um lavar da roupa suja a nvel partidrio. +O crculo vicioso, entre o enunciado da mudana e a prudncia aconselhada pelo momento eleitoral, deixou pouco sabor na boca. +O mau momento seria agravado pelos resultados do primeiro debate televisivo, com um Felipe Gonzlez acabrunhado pela avalanche das crticas dos conservadores. +O programa televisivo Falar Claro viveu, na segunda-feira, um dos seus mais acalorados momentos. +Isto porque o jornalista Joaquim Furtado, sem peias nem concesses como seu timbre, decidiu esta semana tomar o pulso ao estado do futebol portugus. +Espetou duas lminas na cara da boneca. +Deitou-lhe para cima sangue ou tinta vermelha, no se percebia bem. +Comeou a bater-lhe com um martelo. + Cortou-lhe a cabea com uma navalha. +Depenou-a. +Deve ter passado meia -hora. +Vera apagou a luz. +Vestiu o casaco. +Bebeu gua. +Pegou no papel e na caneta e foi-se embora. +Mais tarde disse que teve a sensao que as pessoas estavam espera do climax que no aconteceu. +De facto, estavam. +Vera quis mostrar a usurpao da carne e chamou sua performance Foda de Morte porque num ensaio de Angela Carter sobre Sade, a foda levava morte das mulheres. + a death fuck. +Os automobilistas, esses, continuam espera ... +Trs centenas e meia de pessoas a ver, ouvir e aplaudir o espectculo montado pela Associao Timorense Lafaek. +Ontem noite, num clube desportivo de Darwin, muitos australianos sentaram-se ao lado dos estudantes e levantaram-se para gritar Viva a misso de paz!, Viva Timor-Leste, Viva Xanana, quando est prxima a chegada do Lusitnia. +Crianas e adultos da Associao danaram, vestidas com os trajes e panos coloridos que h sculos os timorenses fabricam. +Leu-se o poeta Borja da Costa e o cntico que Xanana Gusmo escreveu na montanha para a Mulher Timor. +Rui Marques, coordenador da misso que quer ir a Dli, foi chamado para ler a carta enviada clandestinamente pelos estudantes timorenses presos na Indonsia. +Obrigado pela vossa visita a Timor-Leste. +Depois, num cmulo de msica e emoo, cantou-se Peace Mission, o hino composto e ensaiado para o primeiro dos dois dias de festa. +Venham e juntem-se misso de paz. +(...) Duzentos mil j morreram. +Acreditam? +Acreditam? +Num local da enorme sala, um jovem timorense vestido de guerreiro soluou e chorou abraado s pessoas. +O movimento ecologista blgaro Ecoglasnost pediu aos pases da CEE para fornecer gratuitamente energia Bulgria com o objectivo de possibilitar o encerramento da central nuclear de Kozlodoui, situada no Danbio e considerada perigosa pelos peritos blgaros e internacionais. +Os quatro reactores ainda em funcionamento da central, de 440 megawatts cada um, foram construdos entre 1974 e 1975 e inspeccionados recentemente por uma misso da Agncia Internacional da Energia Atmica. +Dois deles esto num estado extremamente envelhecido. +Como que explicamos todas estas medidas israelitas? +Chamamos a isto terrorismo de Estado organizado, acrescentou Arafat. +Enquanto a violncia de palavras entre Israel e a OLP aquece, a violncia continua a marcar a cena no Lbano, onde caas israelitas lanaram ontem mais dois raides contra foras hostis ao processo de paz. +No bombardeamento da base da Frente Popular de Libertao da Palestina- Comando Geral, na fronteira lbano-sria, ficou ferido um guerrilheiro. +No ataque anterior contra uma base do Hezbollah, no vale de Bekaa, Lbano, morreu um guerrilheiro fundamentalista e outros trs ficaram feridos. +Ramos Horta no veio a Portugal s para receber os parabns pelo Prmio Nobel da Paz com que foi distinguido juntamente com o bispo Belo. +13 anos depois da ltima votao sobre Timor-Leste na ONU, chegou o momento de voltar a levar o tema ao julgamento da Assembleia Geral daquela organizao. +A defesa desta estratgia de guerra total no plenrio das Naes Unidas no assenta exclusivamente no impulso que a questo timorense ganhou com a atribuio do Nobel a Horta e a Belo -- Ramos Horta j tinha sugerido esse passo num memorando que fez chegar ao Palcio das Necessidades meses antes de se conhecer a escolha de Oslo --, mas o certo que o Nobel da Paz timorense constitui um reforo importante. + para o aproveitar que Ramos Horta vem agora pessoalmente insistir na sua proposta. +Os que querem passar uma esponja sobre o passado impedem a reconciliao. +Ele sada a esperada vinda do Presidente israelita, Ezer Weizman, s cerimnias do 8 de Maio, como um gesto de reconciliao por parte da Alemanha. +Mas, acrescenta, o ritmo e a forma desta reconciliao no devem ser ditados do ponto de vista dos culpados, mas pelo das vtimas. +Em vez da esperada barracada musical, em Alvalade houve outro tipo de cenas bem menos divertidas. +Por causa do perigo de morte representado pela pala que ameaa que cai mas no cai, alguns milhares de pessoas pagaram para assistir ao concerto num local e acabaram noutro, por falta de espao. +Est tudo cheio, diziam elementos da organizao, tente na bancada do lado. +O mais conhecido dos ardinas de Lisboa, Carlos Francisco dos Santos, foi ontem a enterrar no Cemitrio do Alto de So Joo. +Carlos dos jornais, como todos lhe chamavam, tinha 77 anos e faleceu no domingo, num hospital da capital, em consequncia de uma prolongada doena pulmonar. +Com 57 anos de profisso, Carlos dos Santos, tornou-se popular pelos preges que usava na venda de jornais e de lotaria, na zona da Baixa e Bairro Alto, e pela maneira afvel com que se relacionava com toda a gente. +Fernando foi a figura do jogo de ontem entre o Rio Ave e o Sporting ao marcar trs golos ao clube lisboeta. +Peixe acabou por ser o heri sportinguista, ao defender, no ltimo minuto e sobre a linha de golo, um remate que daria o empate ao clube de Vila do Conde. +No ano passado, os organizadores pensaram em acabar com a corrida, devido insuficincia de apoios. +os Sinos uma das provas de estrada com maiores pergaminhos no nosso pas. +Falta agora que os corredores contribuam, com a sua presena. +No Roubars. +O roubo neste episdio no de bens materiais. +Tudo circula volta de uma criana, abanadonada pela me natural e criada por outra mulher que reconhece como me. +A primeira vem roub-la. +A criana ir depois escolher entre elas. +A Comunidade Econmica, por outro lado, dever preparar as condies para a transio gradual para a livre circulao de mercadorias e servios em todos os Estados que a ela aderirem. +Mais tarde, quando estiverem criadas condies econmicas, proceder-se- criao de um mercado comum de mo-de-obra. +No captulo das bases econmicas, os membros devero acordar aces nos domnios da poltica monetria, financeira, alfandegria e de regulamentao de impostos. +Tambm se prev a criao de uma moeda nica, embora, quem o deseje, possa ter a sua prpria divisa. +Ao sair da penumbra, arrastando uma coisa parecida com nvoa cinzenta e oleosa, suspensa a dois palmos da cabea, Joo Carlos andou trs ou quatro passos e parou com um suspiro, como se as suas pernas utilizassem um sistema de suspenso a ar. + Via-se que, por vrias causas, Joo Carlos se cansa com facilidade, pois tambm baixou os ombros e inclinou o queixo para o peito, sem nunca olhar para os lados das grandes janelas do tribunal. +E depois a juza chamou-o, vendo um homem com uma gabardina enorme, ainda maior que ele, um homem com cabelos grossos e brilhantes como crina de cavalo que tem andado chuva, sem acompanhamento veterinrio e com pouca forragem. +Os olhos dele saam para fora da cara e quase chegavam ao nvel da cana do nariz, no caso de se apanhar de perfil a figura perdida e magrssima de Joo Carlos, que tanto podia ter vinte e poucos anos (estragados), como muitos mais, e de facto tinha. +Com 37 anos, Joo Carlos acabara de ser preso mais uma vez por furto num supermercado. +passou as caixas registadores trazendo escondidas nos bolsos 12 embalagens de 10 lminas de barbear. +Mas poderia vende-las. + alis certo que as venderia num reles mercado negro, com tanto orgulho como, h umas dcadas, uns senhores vendiam numa ruela da Baixa de Lisboa os esticadores de colarinho, e parece que disso viviam (disso e de preservativos clandestinos). +Madrid tambm fechou em alta de 0,55 por cento (mais 2,05 por cento), com o seu ndice geral a atingir 376,82 pontos. +A alta madrilena foi causada pela abertura em alta de Wall Street e pela subida dos mercados obrigacionistas, que compensaram os sustos com o anncio da demisso do general russo Alexander Lebed. +O volume de negcios da sesso de ontem 31,2 milhes de contos. +Em Frankfurt, registou-se uma subida de 0,05 por cento, com o ndice DAX a fechar a 2.176,26 pontos, devido baixa que atingiu Wall Street na quarta-feira. +Nos mercados orientais, Tquio foi a excepo e, ao meio da manh, a bolsa tendia para uma alta marginal, com o ndice Nikkei a marcar 12,07 pontos no fim da sesso da manh. +As baixas dos demais mercados marcaram uma tendncia. +Em Hong-Kong, no entanto, houve uma quebra e o ndice Hang-Seng atingiu 61 pontos negativos, com uma subida posterior para 46 pontos. +Singapura tambm fechou com uma ligeira baixa de 2,54 pontos. +Vance destacou a aceitao pelos beligerantes do envio de uma fora de manuteno da paz e sustentou que o novo cessar-fogo indica, ao contrrio dos anteriores, os procedimentos necessrios para um acordo especfico. +O ponto mais crtico reside agora nas possibilidades em manter o cessar-fogo. +Agora vou para Nova Iorque, onde relatarei a Butros-Ghali [ secretrio-geral da ONU ] o que aconteceu nos ltimos dias, afirmou antes de entrar apressadamente no Mercedes preto, j com o motor a trabalhar. +Antes, tinha-se recusado a comentar quais as suas prximas iniciativas caso o acordo no resulte. +Vincent Askew, com 20 pontos, liderou os Seattle SuperSonics na sua vitria sobre os Los Angeles Clippers no nico jogo da Liga Norte-Americana de Basquetebol profissional (NBA) realizado no domingo. +Terry Dehere, dos Clippers, foi o melhor marcador do encontro, com 24 pontos. +Os SuperSonics alcanaram o seu terceiro triunfo na poca e esto em quarto lugar na Diviso Pacfico. +Quanto aos Clippers, ainda no conheceram o sabor da vitria e seguem com cinco derrotas. +A China quer organizar em 1997 um Grande Prmio de Frmula 1 no circuito de Zhuhai, que tem 4,83km de permetro e est situado perto de Hong Kong. +Nas obras previstas para o complexo, que incluir um campo de golfe e seria terminado em 1996, as autoridades pretendem gastar cerca de 30 milhes de contos. +Se a Federao Internacional do Automvel, que j aprovou os planos de alterao ao circuito, aceder aos sonhos dos chineses, estes garantem a presena de 200 mil espectadores. + Folha marcou em Chaves o seu segundo golo do campeonato, que foi considerado pelo PBLICO como o melhor desta jornada. +Mais um golo conseguido em jogada individual. +O portista pegou na bola na esquerda, progrediu pelo centro do terreno, entrou na rea e, com dois toques preciosos, sentou os dois centrais flavienses, Manuel Correia e Amarildo. +Depois, perante a sada de Orlando rematou de p esquerdo j a curta distncia da baliza. +Depois desta jornada, que rendeu 18 golos, Juskowiak e Marcelo continuam a liderar a lista dos melhores marcadores, ambos com oito remates certeiros. +Os dois jogadores voltaram a marcar no fim-de-semana, curiosamente ambos em bonitos remates de cabea. +Diatriba de amor contra un hombre sentado assinala amanh a estreia na escrita teatral do colombiano Gabriel Garcia Marquez. +Romancista, contista, cronista e guionista, faltava a Gabo experimentar, em livro, o teatro, um meio para o qual outros, por ele, verteram muito dos seus contos. +A tiragem inicial da obra, um monlogo, ser de 50 mil exemplares. +Segundo o jornal de Bogot El Tiempo, este texto de Gabo estar nos palcos em breve, numa encenao a cargo de Ricardo Garcia. +A obra, redigida em 1987, esteve sete anos na gaveta antes de ser editada. +Sete bias de sinalizao da barra do Douro vo ser hoje recolocadas, marcando o percurso da Cantareira at Vila Nova de Gaia. +Cinco desses aparelhos esto equipados com um sistema de iluminao, para que a entrada nocturna de embarcaes se possa fazer com toda a segurana. +As bias tinham sido arrastadas durante as cheias de Dezembro e Janeiro passados e deviam ter sido repostas logo no incio do ms de Maro. +Uma fonte da Capitania do Porto do Douro disse ao PBLICO que o sistema vai entrar imediatamente em funcionamento, colmatando assim o processo de recolocao das bias destrudas pelas cheias, que ocorre desde Fevereiro. +Fica assim totalmente sinalizado o percurso entre a barra do Douro e a Ponte de D. Lus I, para o qual foi necessria a substituio das bias originais por outras de maior dimenso, enviadas pela Direco de Faris. +As novas unidades ficaro seguras por blocos de beto. +Iliescu tinha anunciado que se deslocaria amanh ao vale de Jiu, mas ontem os servios presidenciais disseram que a viagem fora cancelada. +Os trs atletas portugueses em aco no quarto dia de provas no foram felizes. +A sportinguista Teresa Machado foi a primeira a entrar em aco, lanando no segundo lugar do grupo A qualificativo do disco. +a portuguesa, muito nervosa, falhou por completo o primeiro lanamento. +No segundo ficou a mais de dez metros do seu recorde nacional (63,70m) com 53,60m, e s no derradeiro intento (a qualificao s tem trs) se aproximou um pouco mais do que vale, com 56,02m, mas no chegou. +Foram precisos 61,22m para ir final. +Quem brilhou de manh foi Lucrcia Jardim (Benfica), na eliminatria de 200m. +Correu a srie 4 e ganhou russa Natalya Voronova (23,45s) e americana Gwen Torrence, a campe olmpica (23,46s), com 23m45s. +De tarde, porm, tudo acabaria em frustrao. +No terceiro quarto-de-final precisava de uma posio nas quatro primeiras para ir s meias-finais e perdeu esse precioso quarto posto para a jamaicana Dhalia Duhaney mesmo nos metros finais, com 23,11s contra 23,16s. +Ganhou a francesa Marie-Jos Prec com 22,73s. +Resta a pequenssima consolao de ter feito o melhor tempo nacional de 1993. +De que tem medo o IPACA? +O Instituto Portugus das Artes Cinematogrfica e do Audiovisual (IPACA, ex-IPC) tambm no respondeu s perguntas formuladas pelo PBLICO. +Responsveis seus comearam por pedir que as perguntas fossem enviadas por fax, para serem respondidas por escrito -- para no haver mal entendidos. +O fax foi enviado, tal como pedido, presidente do Instituto, Zita Seabra. +Quarenta por cento da gua potvel usada em sua casa vai pela sanita abaixo. +O Presidente da Repblica, Mrio Soares, enviou no incio da semana um telegrama Coreia do Norte, atravs do embaixador de Portugal em Pequim, apresentando condolncias pela morte de Kim Il Sung, soube o PBLICO junto de fontes coreanas em Lisboa. +Portugal tem relaes diplomticas com a Coreia do Norte desde 1975 e representado em Pyongyang pelo chefe da sua misso diplomtica na capital chinesa. +O telegrama dirigido a Kim Jong Il, filho do falecido Presidente da Coreia do Norte e seu presumvel herdeiro. +O primeiro-ministro, Cavaco Silva, fez apresentar na embaixada norte-coreana em Lisboa, atravs do seu adjunto diplomtico, sentidos psames pela morte de Kim Il Sung, falecido h uma semana, com 82 anos. +Entretanto, ontem, a Coreia do Sul, encorajada pelos sinais tornados at agora pblicos sobre o que se vai passando na semana de luto no vizinho Norte, indicando uma aparente passagem calma de poderes de pai para filho, decidiu levantar parte do alerta especial em que tinha colocado as suas tropas, na sequncia do falecimento do Grande Lder. +P -- Mas as Assembleias Municipais s tm parecer vinculativo numa segunda fase ... +R -- Sem dvida. +Mas no faria muito sentido, se as Assembleias Municipais se pronunciarem neste ou naquele sentido j nesta fase, vir a ser aprovada uma lei de criao das regies contrariando uma vontade que se sabe que vai ser manifestada ulteriormente. +Mas o sistema de inscrio foi alterado h cerca de um ano. +Deixou de ser foroso as pessoas deslocarem-se sede para se inscrever em turnos de 15 dias num dos centros de frias. +Agora podem tratar directamente, em contacto telefnico ou por fax, com o centro onde pretendem passar frias. +O sistema antigo s se mantm para os Centros de Entre-os-Rios, So Pedro do Sul e Foz do Arelho. +Ns, dentro de Espanha, sempre manifestmos que deveria haver interesse por Portugal, que sempre teve um certo receio, porque a histria foi o que foi, comenta ao PBLICO Jordi Pujol, se temos uma identidade prpria e no temos medo, muito menos os portugueses, que tm um Estado prprio. + desta forma directa que o presidente da Generalitat, sem dvida um dos dirigentes polticos mais influentes de Espanha, equaciona as relaes entre os dois pases peninsulares. +Pujol recebe oficialmente pela primeira vez o Presidente da Repblica de Portugal -- uma anterior visita de Mrio Soares, no Outono de 1987, foi privada --, mas no esquece o ocorrido h quase nove anos. +A visita de Soares foi privada mas muito positiva para ns, assegura, relembrando tempos de busca de protagonismo que, nos ltimos trs anos, com socialistas e conservadores no poder em Madrid, deram lugar condio de parceiro indispensvel. +Ainda quanto a alteraes na Liga de Clubes, a direco ser reduzida de quatro para trs membros, passando apenas a haver um presidente (eleito em AG), um director-executivo e outro elemento eleito pela direco da Liga, onde tm assento onze clubes. +Este ltimo ser o nico que no coincide com o OA. +De resto, decidiu-se ainda aumentar os elementos da Comisso Arbitral da Liga, que sero nove em vez de sete. +Todas estas decises, disse Damsio, foram tomadas por unanimidade. +A FPF confirmou ontem a repetio do Benfica-Sporting para amanh, no estdio do Restelo, negando assim provimento contestao do Sporting. +A hora do jogo, referente 30 jornada do campeonato nacional e repetido por alegado erro do rbitro na expulso do benfiquista Caniggia, ficou dependente do clube de Alvalade. +A dvida entre as 18h30 ou as 20h30, esta ltima proposta pelo Benfica, para permitir a transmisso televisiva. +Confundir identidade e fisionomia: um sonho tenaz. +Uma vez em interaco com os outros, a gesto das nossas expresses faciais representa um capital simblico que implica um domnio e um controle socialmente construdos. +Os presentes autores, seguindo de perto as coordenadas tericas e os parmetros metodolgicos e cronolgicos de Foucault, propem-nos uma histria do modo como a emergncia da racionalidade moderna instituiu uma tenso constante entre o que orgnico, e como tal objecto de um saber exacto, e o que expressivo, logo passvel de uma hermenutica que valida processos contnuos de reclassificao social. +Trata-se de uma separao politicamente profcua, j que, ao longo de os scs. XVI a XIX, o homem ter interiorizado a necessidade de vigiar a maior ou menor conformidade entre rosto, preceitos ticos e poder social, ao mesmo tempo que lhe pedem que seja singular e autntico. +Uma verdade subjectiva incorporada atravs de normas sociais e, inversa e complementarmente, prticas sociais que avaliam do grau de integrao de cada um. +O cuidado de si como inscrio do poder. +No termo de um Vero em que a febre dos divrcios e escndalos delapidou o patrimnio de prestgio da coroa britnica, foi a vez de a libra cair em descrdito. +Tal como a rainha Isabel II, John Major limitou-se a acompanhar os factos. +Mas se o silncio pode favorecer a recuperao da imagem da coroa, a indeciso mortal, tanto em poltica como em economia. + O primeiro-ministro britnico no pode querer assinar Maastricht e manter a libra fora do SME e da sua correspondente disciplina. +Como no pode continuar a afirmar a prioridade da luta contra a inflao deixando a libra desvalorizar-se. +Menos pode ainda permitir que o seu ministro das Finanas escreva aos colegas de Gabinete notas prevendo que a libra ir continuar fora do SME por meses, talvez anos. +O presumvel autor de 15 assaltos ocorridos recentemente em residncias de Setbal e do Pinhal Novo, atravs da extrao do canho das fechaduras, foi detido pela PJ daquela cidade e viu a priso confirmada pelo tribunal local. +Trata-se de um jovem de 20 anos, que ter confessado os crimes e a venda dos objectos roubados para a compra de herona. +O detido saiu pouco antes do incio da srie de assaltos de uma instituio de recuperao de toxicodependentes, aparentemente reabilitado, e comeara a frequentar um curso de formao profissional financiado pela Unio Europeia. +O facto de os assaltos ocorrerem, por norma, hora do almoo ou em perodos a que o jovem faltava s aulas ps a PJ na sua pista. +No foi divulgado o valor dos objectos em ouro e electrodomsticos roubados. +Os dois autores de um assalto a uma bomba de gasolina da Cepsa em Vila Franca de Xira, ocorrido na madrugada de ontem, foram detidos cerca de uma hora depois na Praa de Espanha, s 4h25, em Lisboa. +Os mesmos indivduos so acusados de outros trs assaltos ocorridos em Coimbra entre o dia 14 de Agosto e a tarde da ltima quinta-feira. +A PSP sabia que os assaltantes se deslocavam numa carrinha Renault Express, facto que a levou a mandar parar o veculo onde seguiam. +Levados de volta a Vila Franca, os assaltantes, de 28 e 31 anos, foram identificados. +Nos quatro assaltos, preferencialmente feitos a postos de gasolina, foram roubados mais de 300 mil escudos. +A viatura em que seguiam era igualmente roubada. +Cerca de metade dos resultados apurados, concretamente sete milhes de contos, vai ser distribuda ao accionista (Estado). +Quanto ao cash flow da EDP, atingiu os 128,3 milhes de contos em 1991. +Estes resultados foram positivamente influenciados pelo acrscimo da procura de electricidade, pela estabilidade dos preos dos combustveis importados para produo de electricidade e pela conteno dos restantes custos de explorao, refere a mesma nota. +Como dividir os ministrios? +Os dirigentes turcos Mesut Yilmaz (ANAP, direita) e Necmettin Erbakan (Refah, islamista) iniciaram ontem o processo de formao de um Governo, decidindo criar uma comisso encarregue de repartir os ministrios entre os dois partidos. +Esta questo esteve na origem das divergncias que surgiram durante as negociaes do fim-de-semana e que quase puseram em causa os esforos para um acordo. +A comisso mista dever iniciar amanh os seus trabalhos que, espera-se, estejam terminados no fim da semana. +Chegmos a acordo sobre a maior parte das questes, s restam algumas divergncias menores, explicaram Yilmaz e Erbakan na conferncia de imprensa conjunta que deram ontem. +Segundo a agncia Anatlia, o Refah aceitou que Yilmaz seja primeiro-ministro at ao incio do prximo ano. +Em seguida, se for aplicado como previsto o princpio da rotatividade, a Turquia dever passar a ter o seu primeiro chefe de Governo islamista. +O nico empate de ontem faconteceu no jogo que ops o Friburgo ao Bayern Leverkusen (1-1). +Anteontem jogaram Werder Bremen-Nuremberg (2-3) e Hansa Rostock-Wolfsburg (3-3). +Os marginalizados do sistema de ensino: que soluo? +Os desfavorecidos: que a soluo para os marginalizados do sistema de ensino? +O Governo admite criar uma linha de crdito bonificado para ajudar a atenuar os prejuzos do temporal que nos ltimos dias afectou as regies do interior Norte e Centro. +Quem o diz o secretrio de Estado da Administrao Interna, Armando Vara, que passou o dia de ontem a inteirar-se dos efeitos da intemprie em Bragana, o distrito mais atingido, mas onde j no h aldeias isoladas. +Um Vestido para cinco mulheres a pea encenada por Diogo Infante que transformou Margarida Pinto Correia numa loura. +Um texto em torno do sagrado matrimnio no qual se discute as possibilidades de encontrar um homem que no seja casado, nem maricas e tenha emprego. +Um teatro assumidamente comercial. +O Hospital de Santo Antnio, no Porto, dever pedir ainda esta semana Lipor para ali depositar as vrias dezenas de toenaladas de cinzas resultantes da queima de lixos hospitalares que, h um ano e meio, se acumulam em contentores de plstico. + o resultado de um ofcio enviado na semana finda pelo Ministrio do Ambiente, o qual atesta a inocuidade daquelas cinzas e aconselha a sua deposio do aterro de Ermesinde. +As escrias do Santo Antnio vo, assim, passar a conhecer um fim semelhante generalidade das cinzas produzidas pelas incineradoras hospitalares. +Mais complicada parece a situao do Hospital de Guimares. +Desde o encerramento, em Julho, da incineradora do Hospital de S. Marcos, de Braga, que ali so tratados os resduos de todas as unidades de sade do distrito, estando a laborar perto do seu limite. +Toxicodependncia que o levara a contrair o vrus da sida. +Cauteleiro de profisso, era, portanto, seropositivo, embora a doena estivesse ainda numa fase controlada. +Nascera na freguesia de Miragaia, no Porto, e estava em Santa Cruz do Bispo. +Na cadeia era bem-comportado e ocupava-se em trabalhos de faxina. +Suicidou-se a 7 de Agosto. +Sem deixar qualquer sinal que exteriorizasse o seu estado de esprito, uma conversa com algum. +Sara da cela s 8h30, pedindo ao guarda para voltar pouco depois, para descansar. +s 9h30, encontravam-no enforcado, com um lenol, nas grades da cela. +O caso est a ser analisado pela Direco Regional de Educao do Centro e o Coordenador da rea Educativa (CAE) j admitiu no haver condies objectivas para que os professores executem as suas tarefas e continuem as actividades lectivas, sobretudo enquanto o director do Colgio do Mondego se encontrar ausente no estrangeiro. +Mas o Sindicato dos Professores da Regio Centro (SPRC) reagiu, pedindo a demisso da direco do instituto de reinsero. +A situao dos professores deste reformatrio complicada e complexa, o que motiva a maior parte dos protestos do SPRC. + que os professores colocados no Colgio do Mondego pertencem escola bsica do 2 e 3 ciclo de Santa Clara, na Guarda, instituio para a qual concorrem. +S que esta EB 2,3 tem um protocolo com o Colgio para a disponibilizao do seu corpo docente. +Sendo assim, vo parar ao instituto de reinsero social professores sem experincia profissional e sem formao adequada situao. + No outro dia, apareceram l dois guerrilheiros do PAIGC. +Um deles olhou para mim e reconheceu-me logo. +Era Oto, um ex-controlador de trfego areo em Bissalanca que se juntara guerrilha. +Deu-me um mao de cigarros e sossegou-me. + No dia seguinte, um domingo, chegmos a Conakry. +Levaram-me ao Ministrio da Defesa para encontrar algum a quem me entregar, mas o edifcio estava vazio. +Demos imensas voltas pelos corredores at os gendarmes decidirem levar-me para uma esquadra de polcia. +No dia seguinte, abriram-me a porta da cela, fizeram-me subir umas escadinhas e entrar para uma sala. +Sentados a uma mesa trapezoidal encontravam-se cinco indivduos de grand boubou [ vestimenta muulmana ] at aos ps, com os seus gorros tpicos, todos muito grandes e com ar srio. +Transportado para os Estados Unidos, foi mantido nove meses numa cela sem luz, vigiado 24 horas por dia por uma cmara de televiso e por guardas prisionais. +Lavagem ao crebro, presso do advogado na altura que lhe deu conta da inevitvel cadeira elctrica na falta de uma confisso, alega o presumvel homicida de Luther King. +James Earl Ray, reza a publicidade da Thames, reclamou a sua inocncia e pediu julgamento. +Ningum ouviu. +At agora. +Os bastidores do negcio que levou entrada do Banco Central Hispano (BCH) no capital do Banco Comercial Portugus foram a gota de gua. +Alis, muito possivelmente vo ser expostos em tribunal. +Tudo porque Neto da Silva, ex-secretrio de Estado do Comrcio Externo e depois lder da Socifa, exige receber uma comisso alegadamente prometida por Amrico Amorim caso conseguisse vender o lote de aces do empresrio no banco portugus. +O que Amrico Amorim no confirma, recusando-se a pagar qualquer prestao de servios. +Tudo comeou quando o rei da cortia concedeu a Neto da Silva um mandato para procurar um comprador para as aces que detinha no BCP. +no havia limite de tempo para a concretizao do negcio, a comisso a receber seria de trs por cento do montante total envolvido, incluindo impostos, e deveria ser observado o mais rigoroso sigilo sobre a transaco. +Marius Weiers, alto funcionrio do ministrio sul-africano do Comrcio e Indstria, disse ontem em Joanesburgo que Portugal rene condies para vir a constituir a porta da frica do Sul na Europa. +apenas 4,6 por cento dos patres so licenciados e s cerca de 30 por cento tm habilitaes equivalentes ao secundrio. +Uma das preocupaes claras do manifesto esclarecer que o congresso no concorre no plano dos partidos polticos. +Como ali afirmado, no cabe exclusivamente classe poltica dar respostas sobre as interrogaes que o futuro levanta, antes exige a participao activa da sociedade civil, que tem igualmente responsabilidades e deveres de que no pode nem deve demitir-se. +Gomes Motta, respondendo s perguntas dos jornalistas, iria mais longe ao afirmar que os partidos no esgotam a actividade cvica do pas e, afastando qualquer mal-estar que o congresso possa provocar nas hostes socialistas por eventualmente ofuscar alguns dos seus projectos, esclareceria ainda que esta iniciativa e as do PS em certa medida completam-se. +Este manifesto ser publicado na ntegra nos jornais, sendo acompanhado por uma ficha de inscrio para participar nos trabalhos do congresso que decorrero no pavilho da FIL, de 8 a 10 de Maio prximo. +At l, sero organizadas sesses em vrias cidades publicitando a iniciativa e recolhendo sugestes sobre a incorporao de temas regionais. +No esquema apresentado, so seis os painis em debate, que abrangem temas que vo desde a cultura, o ensino e a estruturas econmicas at ao Estado democrtico, solidariedade e o papel de Portugal na Europa e no mundo. +Estes so alguns dos dados da avaliao externa feita pelo Instituto de Inovao Educacional (IIE). +O estudo representa um dos mais importantes indicadores das aprendizagens dos alunos, tendo inaugurado a era das provas aferidas no sistema educativo portugus. +recentemente (ver PBLICO de 27/03/97), o ministro da Educao anunciou que seriam lanados testes de aferio de mbito nacional nos 4, 6 e 9 anos. + A inteno figura no documento estratgico para a Educao entregue na Assembleia da Repblica, no qual, curiosamente, se retira a matria da alada do IIE e se entrega ao Gabinete de Avaliao Educacional (Gave). +O relatrio adianta que todos os relatos de tortura e maus tratos se referiam a pessoas que tinham sido detidas sob suspeita de terem cometido delitos criminais, tendo em muitos dos casos sido libertadas sem serem inculpadas. + R. -- lgico que os directos s podem existir se houver regras claras. +E tm de existir porque, neste momento, impossvel produzir um filme sem ser em co-produo e sem dar contrapartidas a realizadores estrangeiros. +Mas acredito que as preocupaes que os realizadores tm, tambm a direco do IPACA as tem. +P. -- Est a falar pelo IPACA? +O espanhol Jesus Montoya (Amaya) venceu ontem a 16 etapa da Volta Espanha em bicicleta, disputada entre Santander e o Alto de Campoo, na distncia de 173,4km e subiu ao 4 lugar da classificao geral individual. +O Banco Nacional Ultramarino (BNU) tinha aprovado at Dezembro de 1991 cerca de seis milhes de contos de financiamentos para linhas dedicadas a pequenas e mdias empresas. +A dotao global das linhas especficas para as PME de 12,5 milhes de contos, oriundos do Banco Europeu de Investimentos, do Fundo de Regularizao da Dvida Pblica e da Caixa Geral de Depsitos. +Este apoio foi decidido em 1990 e inclui o reforo de capitais prprios, linhas de crdito em colaborao com a CGD, tal como os fundos de apoio s iniciativas de jovens empresrios. +Por outro lado, at ao final de 1991 o BNU tinha prestado 292 garantias a favor do Instituto de Apoio s Pequenas e Mdias Empresas, para a liberalizao prvia de incentivos concedidos por este instituto. +Toxicodependncia: falar claro ou confundir? +A toxicodependncia, pelo desespero que provoca, um terreno propcio a todo o tipo de manipulaes e os mass media no tm fugido a esta tentao. +As notcias revelam, salvo raras excepes, um cariz sensacionalista, que alimenta sobretudo a ideia de cura mgica ou o desejo de encontrar bodes expiatrios para a explicao do fenmeno, permitindo, por exemplo, que se fale de eficcia do(s) tratamento(s) de formas pouco honestas, por ignorncia ou por manipulao deliberada. + bvio que quem lucra com esta situao no so os toxicodependentes e as suas famlias. +Um exemplo recente de como se pode confundir a opinio pblica ocorreu no programa de TV Falar Claro, no passado dia 22 de Junho. +O relatrio foi elaborado por trs dos mais respeitados peritos da Europa: John Wilesmith, director de epidemiologia no Laboratrio Central do Governo britnico, Bram Schreuder, do Instituto de Cincia e Sade Animal da Holanda, e C. Straub, do Centro Federal Alemo de Pesquisas sobre Vrus e Doenas de Animais. +O que espanta, porm, no que se pretenda fazer marcha atrs relativamente a um referendo que devia ter sido realizado h j muito tempo. +Bem vistas as coisas, os portugueses dificilmente compreenderiam o facto de serem agora chamados a pronunciar-se sobre algo que h muito foi decidido sua revelia. +Pelo que, muito provavelmente, voltariam a abster-se de ir s urnas. +Talvez prefiram ir comer castanhas para qualquer lado. +O que realmente impressiona que ningum parea inclinado a defender -- por simples analogia -- que uma absteno em massa no referendo das regies por tambm em causa, e de modo irremedivel, o processo de regionalizao. +Exceptuando o PCP e alguns perigosos radicais socialistas, o referendo das regies parece ter sido definitivamente aceite como algo de imprescindvel. +Circunstncia que, tendo em conta a argumentao agora utilizada para tirar a castanha da Europa do braseiro da indiferena popular, s pode ser lida de uma forma: a tendncia dominante da classe poltica j traou o destino velha quimera da descentralizao. +Na melhor das hipteses, o povo, devidamente aterrorizado pelo fantasma do separatismo, chumba o processo. +No pior dos cenrios, o pas volta a abster-se e a regionalizao tem assegurados mais 25 anos de permanncia no fundo falso da gaveta da democracia. +H, mas todas elas esto j transpostas para o direito nacional. +Referem-se a normas de segurana e de sade e higiene, regras de iluminao, etc. +As questes mais complicadas esto ainda sobre a mesa. + o caso da organizao do tempo de trabalho e disposies sobre mulheres grvidas, cuja aprovao no se espera para a presidncia britnica, no entrando, assim, em vigor a 1 de Janeiro de 1993. +s vezes, pensa que isto de imagem de empresa passa tambm por uma boa auditoria e que as empresas de auditoria estrangeiras do melhor nome. + Poder recorrer a elas, mesmo que no estejam sediadas em Portugal? +Os partidos pr-governamentais voltam atrs quanto prometida reviso constitucional. +13 de Maio -- A oposio apela de novo populao para que se manifeste contra o Governo a partir do dia 17. +A formao espanhola do Chapela venceu ontem o Torneio Internacional Feira de S. Mateus, que decorreu em Viseu, ao derrotar na final o Valladolid, por 28-27 (13-13 ao intervalo), na quinta e ltima jornada da prova, na qual participaram ainda FC Porto, Benfica, Sporting e Madeira SAD. +O Feira de S. Mateus foi uma excelente oportunidade de ver em aco estas quatro equipas portuguesas que sero, certamente, uma ameaa hegemonia do tetracampeo ABC. +Segundo um alto responsvel da instituio, que solicitou o anonimato, o ritmo de recuperao da economia mundial dever acentuar-se j no segundo semestre de 1992, opondo-se, deste modo, s teses mais pessimistas que prognosticam fortes probabilidades de uma recesso mundial. +Embora se constatem riscos em algumas zonas, as nossas expectativas so de uma recuperao global, disse a mesma fonte. +Apesar de no ser previsvel que a Europa abrande ainda mais a sua actividade, a retoma do seu crescimento dever ser mais fraca do que em outros pases industrializados, devido insuficincia das reformas estruturais, acrescentou. +Tomando como caso concreto a Frana, com um mercado de trabalho considerado pouco flexvel, o mesmo responsvel do FMI antev que a Europa dever continuar a registar um crescimento lento, caso no concretize rapidamente as necessrias reformas estruturais. +Esta situao faz prever que a Cimeira de Lisboa acabar por aprovar apenas um acordo poltico sobre o Pacote Delors II que constitua um compromisso formal dos Doze relativamente sua futura aprovao detalhada. +A demisso de Hans-Dietrich Genscher e a crise poltica que atravessa o Governo de Bona (tendo em pano de fundo uma derrapagem econmica, resultado da unificao, que est a afectar a Alemanha e a ser inevitavelmente exportada para os outros pases europeus) , talvez, o maior revs que a presidncia portuguesa tem de enfrentar no s quanto aprovao do Pacote Delors (Genscher era um sincero apoiante das novas perspectivas financeiras para a Comunidade) como quanto generalidade dos dossiers que esto em cima da mesa. +A Administrao da Casa Branca decidiu ontem manter algumas distncias face s declaraes do antigo Presidente democrata norte-americano Jimmy Carter aps a sua visita a Pale -- o bastio da liderana srvia da Bsnia nos arredores de Sarajevo --, ao recordar que os srvios bsnios continuam a ser considerados os agressores no conflito que se prolonga h mais de dois anos e meio nesta repblica balcnica. +O Concurso de Dana de Salo para a Terceira Idade outro projecto destinado aos idosos sintrenses, que, previsto para o incio de Fevereiro, levar a diversas colectividades locais todos os que queiram concorrer ou simplesmente trocar uns passos de dana. +Distribudos por vrios escales segundo a idade, aos concorrentes basta pertencer ao universo de cerca de 30 mil reformados do municpio, podendo inscrever-se na altura do baile, a divulgar brevemente por todos as associaes e grupos de idosos. +Na sequncia de anteriores projectos, como as visitas ao Jardim Zoolgico, os espectculos de teatro e um passeio de cacilheiro no Tejo, a aco deste ano, como explica Jaime da Mata, pretende continuar o trabalho j iniciado: Impulsionar uma vivncia que salutar e necessria. +Ainda segundo Travessa de Matos, as cmaras da regio querem que a estrada passe a ter caractersticas de itinerrio complementar e os anteriores responsveis apenas queriam proceder pavimentao do piso a partir de Pinheiro, Pvoa de Lanhoso. +A beneficiao da EN 103 chegou a estar prevista no PIDDAC (Plano de Investimentos e Despesas para Desenvolvimento da Administrao Central) para 1995, tendo as reclamaes apresentadas pelos municpios Junta Autnoma de Estradas impedido a sua concretizao. +Agora vo exigir ao ministro da tutela a urgente rectificao da EN 103, entre Braga e Chaves. +As tcnicas de terapia gentica -- tambm chamada geneterapia -- consistem, em termos genricos, em inserir um gene teraputico nas clulas de um doente. +O gene agora introduzido, de nome p53, considerado como um dos mais importantes genes supressores de tumores, pois comanda a produo de uma protena-chave da regulao da diviso celular. +Recorde-se que a cancerizao se produz quando, por alguma razo, as clulas do organismo comeam a dividir-se de forma descontrolada sem chegarem maturao. +Da que, quando o p53 defeituoso, a protena no fabricada e surge o cancro. +Os cientistas estimam que cerca de metade dos casos de cancros esteja associada a mutaes do gene p53. + No caso do doente agora tratado, que sofre de uma forma comum do cancro do pulmo, o tratamento consiste em administrar vrias injeces do gene p53 normal -- ou seja, dotado da sua aco supressora de cancros -- dentro do prprio tumor, a bordo de um vrus. +Como o vrus tem uma propenso para infectar as clulas humanas, consegue em princpio fazer penetrar o gene dentro das clulas cancerosas, servindo de cavalo de Tria aos genes p53. +Os Estados Unidos reexaminaro as bases do acordo de cooperao econmica concludo com o Japo, se no se alcanarem acordos credveis entre os dois pases durante a cimeira Clinton-Hosokawa, prevista para 11 de Fevereiro em Washington. +A advertncia foi feita ontem em Tquio pelo secretrio norte-americano do Tesouro, Lloyd Bentsen, depois de se ter encontrado a seu pedido com o primeiro-ministro Morihiro Hosokawa e o ministro das Finanas Hirohisa Fujii. +Os investimentos estrangeiros vo continuar a afluir este ano China, mas a um ritmo menos elevado do que em 1993, e o governo vai reforar o controlo sobre as joint-ventures, segundo o jornal China Daily. +A baixa ir dever-se principalmente ao arrefecimento da actividade imobiliria, cujo crescimento exponencial no ano de 1993 se deveu em grande parte a capitais estrangeiros. +Estes investigadores vacinaram quatro macacos com uma estirpe pouco virulenta do vrus HIV2 humano (o vrus da sida mais vulgar na frica), que no provoca a doena nestes animais e que desaparece rapidamente do seu organismo. +A seguir, infectaram-nos com o vrus da sida dos macacos, o SIV. +Quase quatro anos depois, trs dos animais ainda se encontram em boa sade, tendo o quarto morrido h uns meses. +Todos os elementos de um grupo de animais que no tinha sido vacinado com o HIV2 morreram da sida dos macacos, nos meses que se seguiram sua infeco pelo SIV. +Com os meus colegas, tencionamos agora tentar desenvolver vacinas destinadas ao ser humano, e em particular contra o vrus HIV2. +Tratar-se- de vacinas preventivas que podero ser eficazes contra qualquer estirpe do HIV2. +Por outro lado, j estamos a colaborar com colegas do Instituto Nacional de Sade da Guin-Bissau, na seleco de populaes que possam vir a participar em ensaios clnicos, quando tivermos uma vacina potencial. +Reais jias do Norte de Portugal o ttulo de uma exposio que ir decorrer entre os prximos dias 10 e 26 de Novembro no Palcio da Bolsa, no Porto. +Comissariado conjuntamente por J Tvora e Manuel Adlio Valle Gomes, o certame conta com o alto patrocnio dos duques de Bragana, que cederam para a ocasio a tiara em brilhantes do sculo XIX usada por Isabel Herdia no casamento com Duarte Pio. +Segundo Virglio Folhadela, presidente da Associao Comercial do Porto, entidade que promove a iniciativa, a mostra tem como um dos seus objectivos principais revelar a forte tradio nortenha nos campos da ourivesaria e joalharia. +A exposio -- formada sobretudo por objectos provenientes de coleces particulares -- inicia-se cronologicamente no sculo XVII e vem at aos nossos dias. +Entre as jias apresentadas contam-se colares de brilhantes e esmeraldas (sc. XVIII e XIX), peas de ouro popular, trenedeiras, crislitas, laas, condecoraes das ordens de Malta e de Cristo em minas e brilhantes e uma caixa de rap do Rei Carlos Alberto. +Assinale-se ainda a presena de um stand da Christie's no espao da mostra, bem como de um avaliador oficial daquela leiloeira inglesa. +Mais de um milho de contos dever, segundo a Lusa, custar o seguro das obras expostas. +Os independentistas tchetchenos prosseguiram o cerco a diversas posies militares russas, que responderam ao ataque abrindo fogo sobre concentraes de combatentes tchetchenos, segundo o termo utilizado pelo centro de imprensa instalado em Mosdok (Osstia do Norte), quartel-general das foras de interveno russas. +No foram fornecidas informaes sobre o balano destes confrontos. +Segundo a agncia Interfax, a artilharia russa bombardeou na noite de sexta para sbado as localidades de Samachki e Zakan-Iurt (situadas respectivamente a 30 km e 15 km a oeste de Grozni). +Aps o fim do cerco capital tchetchena pelas foras russas, tm decorrido violentos e incessantes combates a sudoeste da cidade. +A artilharia e a aviao russas tm vindo a atacar regularmente povoaes situadas nestas zonas, para onde recuaram os combatentes tchetchenos aps a queda de Grozni. +Os confrontos tambm esto a atingir Argun, 15 km a leste da cidade. +O mar. +Tudo o que tenha a ver com o mar, no mar, ao p do mar. +Quem que espera no encontrar nestas frias? +O meu porteiro. +A stima jornada do campeonato ingls de futebol no trouxe alteraes ao topo da classificao, j que os trs primeiros venceram os seus jogos e mantm as posies relativas. +O Norwich bateu em casa o Southampton por 1-0 e soma agora 16 pontos, o Coventry foi ganhar ao terreno do Oldham por 1-0 e est com 15 e o Blackburn Rovers goleou no seu estdio o Nottingham Forest por 4-1, somando 14 pontos, mas com menos um jogo. +Belmiro de Azevedo, presidente da Sonae, acredita que nos prximos anos os negcios do grupo no Brasil vo crescer at chegarem dimenso que actualmente tm em Portugal. +Vamos ser to grandes no Brasil como em Portugal, disse ontem, no Porto, o patro da Sonae, no decorrer de uma videoconferncia que colocou em dilogo oito personalidades do mundo dos negcios dos dois pases. +So muitos os registos, ao longo de sculos, de inmeras e variadas expresses de pnico e supersties provocadas pela apario sbita de um objecto brilhante, projectado no escuro da esfera celeste. +A sua forma estranha, a mudana de posio relativamente s estrelas e a alterao de forma sugeriam interpretaes de almas vagabundas de grandes homens desaparecidos, ou sinais dos deuses anunciando prxima a vingana de algum comportamento menos ajuizado dos terrestres. +Ora, os ricos no esto para isto. +Como dizia o eng. lvaro Barreto, ser ministro ganhar mal, perder negcios, empobrecer alegremente. +O prprio professor Cavaco, de modesta fortuna, farto de nos aturar, mandou tudo quele stio -- aplicando uma bofetada sem mo aos que vivem da politiquice -- e retomar a carreira profissional ganhando o dobro, chateando-se pela metade. +Resta, por excluso, o governo dos pobrezinhos, humildes mas honrados. +Modelo muito querido ao dr. lvaro Cunhal, mas completamente ultrapassado. +L'Incoronazione di Poppea foi a ltima a ser reeditada e este regresso s discotecas no suporte j irreversvel conquistador do mercado ter de ser saudado com uma efusividade guardada para, e s para, as ocasies muito, muito, muito especiais. +Nunca mais se poder falar, no futuro, da Poppea de Harnoncourt. +Porque, depois deste registo de 1972, ficaria imortalizada em som e imagem -- na realizao visual de Jean-Pierre Ponnelle -- uma aproximao dos finais dos anos 70 deliberadamente expressionista e, agora, em 1993, em Salzburgo, Harnoncourt surpreende (e abre uma inflamada polmica) ao explorar at aos limites do sustentvel uma viso quase grotesca, comportando a contaminao por uma componente burlesca surpreendentemente suportada pelo cinismo da narrativa, com inesperada opulncia de meios instrumentais e com um estranho elenco reunindo intrpretes familiarizados com escolas de canto bem distintas e distantes das requeridas por uma especializao na msica seiscentista. +Os investidores estrangeiros esto de volta ao Brasil. + No centro das atenes esto as privatizaes que h pouco mais de um ms tiveram incio. +Para concretizar os seus investimentos, os estrangeiros aguardam apenas a concluso do acordo entre o Brasil e o Fundo Monetrio Internacional (FMI) a realizar ainda este ms, que trar ao Brasil mais trs mil milhes de dlares. +Alguns investidores externos aguardam apenas pela luz verde para a concretizao do emprstimo, interpretando de forma positiva o facto de o FMI estar disposto a aceitar a carta de intenes do governo de Collor de Mello, e a disponibilidade deste para aceitar o tratamento de choque que ser imposto economia brasileira. +Quantos melhores jogos j aconteceram neste Mundial? +O Nigria-Espanha e o Inglaterra-Romnia, da primeira fase? +O Holanda-Jugoslvia e o Argentina-Inglaterra dos oitavos-de-final? +O Brasil-Dinamarca? +Ou, o at aqui melhor de todos, o Holanda-Argentina que, por si s, justifica a existncia de um campeonato e a paixo que todos temos? +Este futebol que apazigua os desejos e termina com as nostalgias foi lanado desde o primeiro segundo. +O tempo de estudo, esses aborrecidos, inteis e interminveis minutos iniciais, deram lugar ao jogo claro. +Uma densidade construtiva, um vocabulrio variado e extremo, uma intensidade magnfica, difcil de atingir. +Poder-se- ir mais longe? +Quando, aos 38', Veron decidiu uma pequena pausa em dois passes laterais sem progresso, levou uma monumental assobiadela. +Ningum queria que aquilo parasse. +O teatro de Beaumarchais. +A msica de Mozart. +O cinema de Renoir. +A Regra do Jogo a sntese perfeita do esprito dos dois primeiros na arte do ltimo. +Toda a comdia humana numa dana beira do precipcio, a guerra que se avizinha. +Um filme premonitrio, que ao tempo foi proibido e mutilado pela censura. +Assim, de acordo com informao da CML, na Rua Edison inverte-se o sentido, passando a circular-se da Av. de Roma para a Av. de Madrid. +Na scar Monteiro Torres, o troo compreendido entre a Rua Oliveira Martins e a Av. de Roma volta a ter sentido nico, circulando-se da Oliveira Martins para a Av. de Roma. +No outro lado da guerra, em Belgrado, a eleio do escritor Dobrica Cosic, um ex-prximo de Tito, como Presidente da nova Jugoslvia foi cumprida, como previsto, pelo parlamento. +Cosic era o nico candidato e os diplomatas ocidentais notam que o seu poder , na prtica, fictcio, pois o homem que mexe os cordelinhos em Belgrado continua a ser o Presidente srvio, Slobodan Milosevic. +A contestao a este continua a subir de tom e ontem, pelo terceiro dia consecutivo, as ruas de Belgrado foram o palco de manifestaes exigindo a sua demisso. +Mais de dez mil estudantes exigiram pacificamente o afastamento de Milosevic, a formao de um governo de salvao nacional e a realizao de eleies. +Decretaram uma greve, ocupam trs faculdades e prometem resistir at satisfao das suas exigncias. + Hoje de manh, o dia do encontro de Fernando Nogueira com o Presidente da Repblica e o ministro j manifestou grande curiosidade quanto ao que Ben Ali ter para lhe dizer. +Segue-se a reunio com o secretrio-geral do partido no poder (RCD, Rassemblement Constitutionnel Democratique, Liga Constitucional Democrtica), e a entrevista com o ministro de Estado e do Interior, Abdallah Kallel, tido como brao direito do Presidente e o nmero dois do Governo. +Um dos resultados prticos desta fuso que Michael Eisner, presidente da Walt Disney, se tornou da noite para o dia o homem mais poderoso do sector. +Nada mau para quem era acusado de ter demasiadas cautelas na conduo do seu grupo e de estar tolhido por uma notvel falta de viso global. +o seu amigo e colega de direco Frank Wells morreu num desastre areo e, logo depois, o director dos estdios, Jeffrey Katzenberg (responsvel pelo renascimento da animao na Disney), bateu com a porta, insatisfeito por no chegar ao topo da empresa, fundando a produtora SKG-Dreamworks com Steven Spielberg e David Geffen. +Di Matteo recebeu a bola na sua intermediria, progrediu pela zona central e, a 25 metros da baliza, arrancou um tiro fulminante que s parou nas redes. +Estavam decorridos apenas 42 segundos do jogo da final da Taa de Inglaterra em futebol, que opunha o Chelsea ao Middlesbrough. +No fim, o Chelsea juntou a este golo mais um, de Newton (83'), e conquistou o trofu pela segunda vez na histria. +A circular justifica a iniciativa por terem surgido alguns problemas no tratamento de Testemunhas de Jeov, quando imperativo ou h a eventualidade de se administrar sangue, como medida indispensvel manuteno da vida. +Em declaraes ao PBLICO, Francisco Costa, membro da comisso de ligao do Hospital, um rgo de contacto entre os hospitais e as Testemunhas de Jeov, disse conhecer o documento, que, no essencial, reitera uma prtica utilizada anteriormente pela direco do hospital, excepo do que se passava com o tratamento de crianas. +Enquanto at aqui o HDL tinha em ateno o parecer dos pais no tratamento de crianas a necessitar de sangue, de agora em diante os mdicos esto livres de o fazer. + Francisco Costa, h trs anos na comisso de ligao hospitalar criada pelas Testemunhas, refere que a posio do HDL no tem semelhanas com os procedimentos de outros hospitais dos distritos volta, citando os casos de Coimbra e Santarm. +P. -- Como economista, acredita que isso possa acontecer? +R. -- No, no acredito. +Nunca quis assinar uma carta de intenes com o FMI que colocasse metas que no pudessem ser cumpridas. +Se o acordo for cumprido, tal com ele foi escrito, a consequncia vai ser mais recesso, maior aperto. +Os indicadores apontam para uma quebra na actividade econmica, para um aumento do desemprego e da inflao. +O acordo inclui um maior aperto fiscal e logo a diminuio dos gastos e da actividade e da oferta dos servios de infra-estruturas. +O acordo indica que vamos continuar num processo recessivo. + As metas quanto inflao conseguir que esta, at ao final de 1992, se situe nos 12 por cento ao ms. +P. -- Porqu? +A confirmarem-se os confrontos, sero o primeiro incidente fronteirio grave desde a morte de oito diplomatas e um jornalista iranianos no Afeganisto, no princpio de Agosto, o acontecimento que agudizou a crise entre os dois pases. +Desde ento, Teero, que viu derrotados os seus ltimos aliados na guerra civil afeg, e Cabul, com a confiana renovada e o apoio do Paquisto, envolveram-se numa escalada de ameaas verbais, concentrando ao mesmo tempo frente a frente poderosos efectivos militares -- os maiores desde o fim da guerra entre o Iro e o Iraque, em 1988. +As famlias ali residentes, que actualmente sobem a p cinco andares por escadas de madeira que ameaam ruir, vo passar a ter elevador e vero o interior das suas residncias modernizado, com a instalao de casas de banho e cozinhas devidamente equipadas. +O lanamento do projecto de recuperao, por concurso pblico entre arquitectos, ter lugar em Julho, estando previsto o incio das obras, oradas em 75 mil contos, para o Vero de 1997. +Joo Pinto -- Antnio Oliveira no lhe dever fazer a mesma surpresa de Carlos Queiroz que, para admirao de todos, em Junho deste ano o remeteu para o banco dos suplentes no encontro com a Estnia, de apuramento para o Mundial dos Estados Unidos, trocando-o por Abel Xavier. +O capito portista continua numa forma excelente e, apesar dos anos, no perdeu a velocidade, percorrendo o seu flanco com grande -vontade. +O jovem sportinguista Nlson ter de esperar. +Hlder -- O central benfiquista ter mais dificuldades em conseguir uma cadeira no onze do seu clube -- onde os lugares parecem estar destinados dupla brasileira Mozer / Paulo -- do que na equipa nacional. +Uma situao complicada porque Hlder se tem mostrado, neste incio do campeonato, como um dos melhores jogadores do Benfica. +Se, como se prev, a Irlanda do Norte jogar com dois pontas-de-lana, Hlder ter funes de marcao e a deve jogar mais em antecipao para evitar descuidos por alguma lentido. +Quem culpabiliza o exterior pelas suas prprias falhas est na verdade procurando desculpas para o seu prprio insucesso. +As etapas boas e ms por que cada indivduo tem de passar fazem parte do sentido delineado para a sua prpria vida. +Isto , necessrio para essa pessoa ter de enfrentar essas circunstncias e prosseguir sem apego. +(Atitude basicamente budista em relao a todas as coisas da vida, porque as desagradveis causam repulsa, e as agradveis tristeza quando j se as no tem). +O amor deve servir como pano de fundo vida de cada pessoa. +Claro que se evoca aqui um amor justo e totalmente abrangente, com base em regras ticas universais. +As coisas que ocorrem ao longo da vida consideradas como desagradveis devem ento ser encaradas como um novo passo na aprendizagem existencial, algo pelo qual temos de passar para evoluir. +No h que sentir revolta, desnimo nem tristeza, devendo procurar-se ver sempre o outro lado da moeda. +At l, no entanto, o imenso pas precisa da ajuda do Ocidente e, se bem que esteja convencido da irreversibilidade das reformas em curso, o Presidente russo no deixaria de argumentar que, se elas falharem, a haveria que pagar dez vezes mais do que os investimentos que agora so necessrios para sustentar a reforma. +No encontro com a imprensa, Kohl afirmaria que foi a primeira vez que houve um debate com total sinceridade e boa vontade, num respeito total por cada um dos parceiros, at porque a Rssia entrou na via da democracia, de um Estado de direito e do respeito pelos direitos humanos. +Em Castelo de Vide, no Centro Municipal de Cultura, est patente a exposio Diversidades ... com Mestre, com obras de pintura de Martins Correia. +As motos BMW fazem 75 anos de existncia e o Centro Comercial Colombo comemora esse aniversrio com uma exposio histrica, na Praa Trpico de Cncer (a praa central do Centro). +Ocasio para ver 22 modelos antigos e oito recentes de motos BMW. +Em verdade vos digo que Indiana Jones e os seus mulos ainda esto na fase do desmame se os compararmos com alguns heris do passado, do serial ou do filme de aventuras. + Neste ltimo caso, o destaque vai para um filme de guerra, Jornada Trgica, que ter menos a ver com o conflito a que se referia do que com o western ou a floresta de Sherwood. +Errol Flynn ps de lado o arco e as flechas, mas levou o mesmo esprito a bordo do bombardeiro. +Ao vermos filmes como Jornada Trgica, Objectivo Burma e outros Sargentos Imortais, podemos interrogar-nos como foi possvel que a Alemanha e o Japo tivessem resistido tanto tempo a estas pelotes de indomveis patifes. +Vamos para a Austrlia despachar os japoneses! +Promessa cumprida apenas com um desvio na rota, porque a segunda incurso de Flynn na guerra foi na Birmnia, onde faz uma razia entre os filhos do Sol Nascente. +O filme chamou-se Objectivo Burma e, embora mais srio, resultava no mesmo. +Realce ainda para o mau dia de Marco Pantani, que apenas veio a Espanha para se preparar para o Mundial da Colmbia. +O italiano chegou na 66 posto, a 13m45s do primeiro, e agora 27 na geral, quatro lugares abaixo de Zuelle. +Hoje, o peloto tem mais uma etapa no difcil traado dos Pirenus, com incio em Naut Aran e final em Luz Ardiden, na parte francesa, aps 179km. +Destaque para a subida do Tourmalet, a 2115m de altitude, que uma das montanhas mticas da Volta Frana. +Para a Quercus, as 40 incineradoras de resduos hospitalares existentes no pas so ilegais. +Na Procuradoria Greal da Repblica deu j anteontem entrada um processo criminal contra o Hospital Jlio de Matos, de Lisboa. +Um momento histrico como pode classificar-se a presena do Castelo da Maia na final-four da Liga das Taas em voleibol, que entre hoje e amanh decorre na cidade de Cuneo, no Norte de Itlia. +Depois de terem terminado a fase regular s com uma derrota nos sete jogos realizados, os maiatos no devero ir muito mais longe. +Com adversrios como os gregos do Olympiakos, os espanhis do Gran Canaria e os italianos do Alpitour Cuneo (actuais detentores do trofu), ao Castelo da Maia pouco mais resta do que tentar ter uma participao digna. +A criao de um Conselho Nacional do Ambiente seria um passo decisivo para criar um frum onde se cruzassem todas as foras que atravessam horizontalmente as questes de ambiente e desenvolvimento. +Contribuiria, a meu ver decisivamente, para um distender de tenses e limar de arestas entre os planos de desenvolvimento e a correcta gesto e preservao de bens alimentares, e seguramente para o indispensvel dilogo entre foras que dele tm andado arredias. +As experincias do Conselho Econmico e Social ou do Conselho Nacional de Educao, com todos os seus eventuais defeitos, so sem dvida um indicador da possibilidade desta inovao. + tempo de desdramatizar o ambiente e de o encarar de uma forma racional e corajosa, no basta o bom senso. +A ver vamos. + Em 1990, existiam 800 supercomputadores em todo o mundo, distribudos por grandes empresas ou organismos pblicos que investem nestes equipamentos para os disponibilizar a instituies cientficas que pagam pelo tempo de utilizao -- como a FCCN em Portugal (ver Universidades nacionais sem supercomputador). +Para o presidente da FCCN, o nmero destas mquinas mais ou menos constante, porque, conforme se vo desenvolvendo novos equipamentos, outros deixam de se enquadrar na definio de supercomputador. +As duas grandes famlias da supercomputao so as chamadas mquinas de multiprocessamento vectorial (como alguns Cray) e as de processamento paralelo -- de que a mais conhecida a Connection Machine, da Thinking Machines. +Ao contrrio do processamento vectorial, em que vrios processadores utilizam a mesma memria -- e onde, na opinio de Heitor Pina, apenas podero existir progressos marginais --, as mquinas de processamento paralelo, surgidas nos anos 60 e generalizadas no final da dcada de 80, tm uma memria dedicada para cada processador, acabando com os engarrafamentos na partilha da memria. +Falamos, obviamente, de Nuno Gomes, que marcou quatro golos num jogo do campeonato, selando a vitria do Benfica sobre o Varzim. +O jovem de Amarante, 21 anos feitos em Julho, custou ao Benfica cerca de 600 mil contos e demorou a mostrar servio. +com Souness e o seu futebol de cruzamentos, Nuno Gomes no era a primeira opo mas, provavelmente, acabar at por ter mais facilidade em marcar golos. +O treinador ingls liberta mais a equipa, joga com mais unidades na rea e favorece o ponta-de-lana. +Nuno Gomes marcou quinze golos no Boavista da poca passada, sobretudo atravs de uma segunda parte da poca -- com Mrio Reis -- muito forte, numa equipa que tinha ainda Jimmy. +Era uma dupla terrvel, que se completava pela potncia do holands e o jogo mais tcnico do jovem portugus. +Funcionou s mil maravilhas e o Boavista acabou por ganhar a Taa de Portugal com dois golos de Sanchez e um de Nuno Gomes. +Deparamos ento com uma situao caricata. +Os israelitas dizem que no abandonaro a sua zona de segurana enquanto os srios continuarem a ocupar o Lbano. +S que, ao mesmo tempo, pedem aos srios que dominem o Hezbollah, reconhecendo implicitamente que s Damasco pode pacificar a sua fronteira mais vulnervel. + possvel que os srios tenham permitido, ou at mesmo fomentado, esta subida de tenso para obrigar os israelitas a admitir que o Lbano um protectorado de Damasco. +Numa negociao sempre bom ter dois ferros no fogo, observou o jornalista francs Patrice Claude. +Como corolrio inevitvel do processo, e na impossibilidade de injectar mais capitais prprios, a administrao da Jotocar decidiu solicitar em tribunal um processo de recuperao, lamentando os inconvenientes que esta situao no deixar de trazer aos credores. +Quanto data da reabertura da empresa, como disse ao PBLICO um responsvel da Cuf-Txteis, ainda imprevisvel. +Teixeira da Mota, porta-voz do BFE, limitou-se a dizer, sobre este assunto, que, aps seis meses de efectiva gesto da actual administrao, concludos em Agosto de 1992, o banco considerou o processo encerrado e que o comprador confirmou ao BFE, nesse mesmo ms, a plena viabilidade e eficcia do contrato celebrado entre as partes. +E mais no disse. +E as variaes dos papis no foram mais expressivas. +O vector accionista atravessa, de facto, um mau momento, apresentando permanentes hesitaes. +Com muitos dos papis com a cotao interrompida em consequncia do perodo de pagamento de dividendos, o negcio continuou pouco expressivo, sem profundidade. +No se prev, alis, que at ao final do ms se registem alteraes com significado. +O ndice BVL Geral encerrou em baixa, cotando-se nos 973,03 pontos, menos 0,23 por cento, enquanto o BPA Contnuo cedeu 0,18 por cento ao fixar-se nos 154,72 pontos. +Em termos de totais, na Bolsa de Lisboa intermediaram-se 10,592 milhes de contos, mais 258,62 por cento. +A Bolsa do Porto encerrou com 5,094 milhes de contos, mais 215,44 por cento que na segunda-feira. +Mesmo antes de jogar, o FC Porto j estava a ganhar com a viagem Crocia. +Os responsveis pelo futebol croata gostaram de rever Ivic e, principalmente, da coragem revelada pelos portistas ao aceitarem jogar numa zona de conflito militar premente, e j garantiram o direito de preferncia ao FC Porto na escolha de futuros talentos. +Uma bom investimento na terra de onde saram Boksic, Prosinecki, Boban ou Suker ... +Um lugar na primeira linha da grelha de partida na contratao de futuros talentos croatas foi, para j, o que o FC Porto conseguiu com a visita a esta ex-repblica jugoslava. +Afastado das competies internacionais desde 1990 -- a seleco jugoslava apurada para a fase final do Europeu da Sucia ficou em casa ltima hora e foi substituda pela surpreendente Dinamarca --, o futebol croata tenta agora voltar ao circuito europeu. +E a presena do FC Porto faz parte dessa tentativa, com a particularidade dos drages no cobrarem cachet para serem a primeira equipa de nomeada a visitar o pas. +Se for aprovada a verso final do anteprojecto de lei de liberdade religiosa -- que hoje ser apresentada publicamente --, os crentes que o desejarem podem passar a indicar que destino pretendem para uma quota equivalente a 0,5 por cento do imposto sobre o rendimento das pessoas singulares (IRS). +De acordo com o texto proposto no ponto 3 do artigo 31, os contribuintes podem indicar qual a igreja ou comunidade religiosa radicada no pas, a inscrever na declarao de rendimentos, que desejam que receba aquela dotao para fins religiosos ou de beneficncia. +O valor de 0,5 por cento no aleatrio. +Ele corresponde sensivelmente ao valor actual da devoluo do IVA, que o Estado faz Igreja Catlica, de acordo com a interpretao que tem sido feita da Concordata. +foi essa a opo em Espanha e Itlia, pases com acordos de Concordata semelhantes a Portugal. +Desde h poucos anos, em ambos os pases optou-se por a possibilidade de cada pessoa indicar, na declarao de rendimentos, qual o fim que pretende dar aquela percentagem -- 0,8 em Espanha, 0,523 em Itlia. +Destaque: Grande parte da imaginao da infncia passa-se na descoberta e na contestao da mentira das regras que o mundo lhe impe. +noutras encaram-nas como um jogo e respondem-lhes jogando tambm, mas sua maneira. +O viaduto da Infante Santo ter uma faixa em cada sentido, dispondo tambm de um passeio para pees. +A sua abertura ao trfego estava inicialmente prevista para o ms de Novembro. +A medida preconizada prende-se com o objectivo de se criaram mais empregos e visa, sobretudo os pases da Comunidade Europeia que, entre 1972 e 1992, viram aumentar o nmero de postos de trabalho em cinco por cento, contra uma taxa de 37 por cento nos Estados Unidos, Canad e Japo. +Para combater o mal -- que s nos 24 pases da Organizao para a Cooperao e Desenvolvimento Econmico (OCDE) abrange 36 milhes de pessoas --, o FMI sugere a promoo da flexibilidade no mercado de emprego, cuja aplicao dever levar diminuio de regalias sociais, mas, em alguns pases, preciso retirar as restries aos horrios de trabalho e sobre os empregos em ' part-time '. +Defende o Fundo que h uma necessidade urgente em quase todos os pases de reexaminar o financiamento e a generosidade global dos regimes de segurana social, com o objectivo de eliminar os elementos que desencorajam a criao de novos empregos. +No relatrio citam-se os elevados encargos sociais das empresas, a generosidade dos subsdios de desemprego, o muito elevado salrio mnimo e as regras de proteco de emprego, consideradas muito rgidas, como factores desmotivadores da criao de postos de trabalho. +Entretanto, a Junta de Freguesia de Riba de Ave foi recebida anteontem por Agostinho Fernandes, a quem se manifestou preocupada com a situao e props o encerramento da ETRSU at que seja reparada a avaria. +O senhor presidente disse-nos que o que se est a passar inadmissvel e que a populao tem razo para estar preocupada, mas adiantou-nos que est convencido de que se trata de um problema tcnico, declarou-nos Miguel Lopes, cujo pedido para suspender a laborao da estao at reparao definitiva da avaria no recebeu uma resposta concreta. +O presidente da Cmara, alis (na linha do que dissera ao PBLICO o director regional do Ambiente do Norte, Guedes Marques), afirmou Lusa que uma avaria pontual nunca poder justificar o encerramento de uma estrutura deste tipo [ ETRSU ]. +O Conselho de Arbitragem da Associao de Futebol de Lisboa apoiou ontem Jorge Coroado e contestou a Comisso de Arbitragem da Liga Portuguesa de Futebol Profissional, que excluiu aquele rbitro do jogo FC Porto-V.Guimares depois de ter montado um esquema para culpar Coroado de fugas de informao. +O conselho lisboeta considera censurvel o comportamento de Coroado, mas recorda que no est prevista nas normas qualquer pena para a infraco que este ter cometido, pelo que a comisso da Liga no podia t-lo excludo do jogo. +Por isso, condena as interpretaes de convenincia da lei e pe os seus servios ao dispr de Coroado, para apoiar o rbitro em qualquer aco disciplinar que porventura lhe seja injustamente instaurada. +O volume financeiro em entradas de cinema atinge 5000 milhes de dlares, enquanto o mercado dos videojogos vale 13 mil milhes de dlares anuais (7000 milhes gastos em salas de jogos e mais 6000 milhes em videojogos domsticos). +Por outro lado, segundo dados da editora Capcom citados na revista Business Week, foram vendidas 12 milhes de cpias de Street Fighter em todo o mundo, havendo 25 milhes de norte-americanos que j o jogaram. +A Capcom cr que, com estes nmeros, no ser difcil recuperar os 40 milhes de dlares investidos no referido filme. +Segundo o presidente da empresa, Kenzo Tsujimoto, o que se ganha na produo do filme o conhecimento e a experincia de que a Capcom necessita para fazer videojogos no futuro. +Nem de propsito, na Primavera sair Street Fighter III: The Movie Game. +Mas a dificuldade desta opo assumidamente militante da organizao est nessa nova vocao dos portugueses ter de nascer durante o curto perodo em que a exposio estiver aberta ao pblico. +Na verdade, a organizao da exposio tem de juntar duas realidades que sempre andaram afastadas: arquitectura e grande pblico. +O desafio posto organizao da exposio ento levar os muncipes de uma cidade a apaixonarem-se perdidamente por um edifcio ao ponto de sarem para a rua em sua defesa. + por isso que a exposio Cassiano Branco e o den -- Lisboa 1991 resultou na maior operao de mediatizao da arquitectura jamais vista em Portugal. + A grande dvida que circula entre a comunidade timorense a razo que teria levado esta mulher doente -- Wendi tem esclerose mltipla -- a escolher o jornal Kompas para prestar tais declaraes, pois se, em tudo isto, existe um fundo de verdade, um peridico indonsio seria a ltima escolha para algum que quer ter o mnimo de credibilidade, comentam. +[ Jos Ramos-Horta reagiu j, acusando Wendi Holland de estar a ser utilizada pela inteligncia indonsia. +Num depoimento telefnico prestado ao CMR -- o primeiro rgo da informao estrangeira a divulgar o contedo da entrevista publicada pelo jornal indonsio --, Horta disse tratar-se de uma campanha que se arrasta h vrios meses e foi lanada pelo jornalista Petrus Suriadi, que esteve recentemente em Portugal. +A Comisso Europeia divulgou na semana passada as novas previses de crescimento para as economias dos Doze em 1994. +Para o crescimento mdio apontada agora uma estimativa de 1,6 por cento, superior em 0,3 pontos previso adiantada anteriormente, no Outono de 1993. +Mas enquanto a evoluo mdia foi revista em alta, os valores para Portugal registaram um recuo ligeiro: dos anteriores 1,4 por cento para 1,25 por cento. +O trabalho dirio de Peter Williams construir mundos e imaginar o modo como as pessoas vo interagir neles (ou como pensa que isso vai acontecer). +Deuses em ascenso, os criadores de realidades virtuais vo influenciar as sociedades futuras com as imagens que criarem. +Se j se do conta disso, algo que no deixam transparecer ... + Numa casa georgiana com vista para o Tamisa, fora de Londres, coexistem dois mundos. +Para os diferenciar, tabuletas indicam o Real World e, outra, apontada ao estdio, o Virtual World. +Carlos Cidade, Linhares de Castro, Lus Janurio, Leal Amado e Moura e S -- o nico que no ex-militante do PCP -- so os nomes escolhidos e j ratificados por o Ncleo de Coimbra da Plataforma de Esquerda para integrar as listas do PS cmara e assembleia municipais da capital do distrito. +A Plataforma de Esquerda dever ainda ficar representada nos concelhos de Montemor-o-velho, Lous, Condeixa e Cantanhede, Arganil, Figueira da Foz, Miranda do Corvo e Soure. +Essencialmente no concelho de Coimbra e em vrios casos, a PE poder mesmo encabear a candidatura presidncia das juntas de freguesia. + a primeira vez que feita uma perseguio federal a organizadores de apostas na Internet. +Nos sites das empresas explicava-se como se podia apostar sobre os resultados de jogos profissionais e universitrios de futebol, basquetebol, hquei e basebol. +Os apostadores teriam de abrir uma conta e depositar a entre 1000 e 5000 dlares (185 a 925 contos). +As apostas sobre os resultados do jogo, feitas por telefone ou pela Internet, custavam entre dez e 50 dlares (1.85000 a 9.25000) -- valores sobre os quais as empresas retinham dez por cento. +Os eventuais ganhos seriam depositados nas contas bancrias dos apostadores ou enviados por correio. +Cada um dos acusados arrisca-se agora a uma pena mxima de cinco anos de priso e a uma multa que pode ir at aos 250 mil dlares (45 mil contos). +Mas Benjamin Brafman -- advogado de Jay Cohen, presidente e proprietrio de uma das empresas (a World Sports Exchange) -- no tem a certeza de que o Governo norte-americano tenha jurisdio neste caso. +Mary Jo White considerou no entanto que as empresas no estavam protegidas pelo facto de a sua sede se situar fora dos Estados Unidos. +Ao longo da semana o Banco de Portugal cedeu liquidez num montante superior ao da semana anterior, tendo mantido as taxas. +No que concerne Dvida Pblica corrente, realizaram-se trs leiles de Bilhetes do Tesouro. +e no terceiro, a 182 dias, foram colocados 25 milhes de contos, taxa mdia de 10,48 por cento, o que acabou por reflectir uma subida das taxas em relao aos leiles anteriores para idnticos perodos. +O abortamento tem sempre uma justificao. +Bem basta sofrla, bem basta ter de o realizar por causa dela. +Digase, de uma vez e claramente, o que se quer ou o que se quer mais. +As reformas a levar a cabo podem conduzir ao desaparecimento do ENDA e sua substituio por outro rgo, que englobe as diversas tendncias do movimento associativo universitrio. +Quando as coisas no correm bem, preciso mudar alguma coisa, justificou um dos participantes num encontro de dirigentes associativos que decorreu durante o fim de semana nas instalaes da Universidade de Aveiro. +A realizao de um inqurito destinado a apurar o encargo mdio mensal de cada estudante universitrio foi outra das medidas sadas do encontro de Aveiro. +Pretendemos definir com o mximo rigor os custos mdios de cada estudante, conforme a regio onde se encontra, pois actualmente no existem dados concretos sobre o assunto, esclareceu Miguel Rodrigues, que tambm presidente da Associao Acadmica da Universidade de Aveiro. +As concluses do inqurito -- que ser realizado com o apoio do Conselho de Reitores -- serviro de base a uma proposta a apresentar ao Ministrio da Educao relativa ao montante das bolsas de estudo e propinas. +J no prximo dia 25, os dirigentes associativos iro reunir-se com o ministro Couto dos Santos para discutir, mais uma vez, o problema do pagamento das propinas universitrias. +De facto, os indcios acumulam-se nesse sentido. +No s por aquilo que se viu ontem nas fortalezas tradicionalmente inexpugnveis do PCP, mas tambm pela receptividade que a campanha de Torres Couto est a encontrar e, sobretudo, pelo que as sondagens comeam a indicar. +Segundo o estudo publicado ontem pelo Expresso, a CDU perderia em Setbal quatro dos sete deputados de que dispe, passando o PS exactamente para a situao inversa. +Em 6 de Outubro, ver-se- se estamos mesmo perante uma dbacle comunista em Setbal. +No bvio primeira vista, mas uma observao mais cuidada no deixa dvidas. +O portal da Quinta do Castro, o nico que resta dos dois que j existiram, est acentuadamente inclinado e pode cair a qualquer momento. +Um aterro feito durante a construo da nova estrada Valena-Mono est a pressionar a estrutura para trs e ir, inevitavelmente, provocar o seu desabamento. +O muro contguo ao portal, parte da estrutura original, foi desmantelado durante a implantao da nova via, obra que esteve a cargo das empresas Soares da Costa e Monte & Monte. +No seu lugar, surgiu um muro feito de blocos de cimento. +Os pedaos da estrutura original foram escondidos pela zeladora nas proximidades, cobertos por mato, prevenindo eventuais furtos da pedra trabalhada. +De acordo com Manuel Cunha, o actual presidente da Junta de Freguesia, durante as obras, a empresa responsvel necessitou de retirar saibro da zona onde o muro se encontrava para efectuar um aterro. +Escavou quanto quis, imediatamente atrs do muro que deitou abaixo para as mquinas poderem passar -- diz o autarca que com a promessa de, depois, reconstruir o muro. +Em 1990, a estrada foi inaugurada e, quatro anos depois, tudo est na mesma. +alguns pases, mas sobretudo a Alemanha, pretendem evitar que a moeda nica assuma logo desde o incio o mesmo estatuto legal que as divisas nacionais, neste caso, o marco. +Se os Quinze conseguirem resolver estes trs problemas, resta apenas cimeira de Madrid escolher o nome da moeda nica, tendo em conta que a denominao ecu, expressa no Tratado de Maastricht, rejeitada pela Alemanha, que prefere euro. +R. -- Porque o simbolismo das escadas est em todo o lado: servem para descer ao inferno ou subir ao cu. +Sobem-se os degraus para chegar ao sucesso ou descem-se, em caso de fracasso. +P. -- Quais as suas influncias no campo da pintura? +R. -- O PC serviu-se da extrema-esquerda, como ponta-de-lana, mas depois foi vtima da sua impreparao e do seu espontanesmo. +Quando, noite, os Comandos atacam a Polcia Militar, outra vez o Costa Gomes que, de madrugada, consegue convencer o Partido Comunista a desistir. +P. -- Com que contrapartidas? +Esta uma das afirmaes principais da posio da Igreja Adventista do Stimo Dia sobre o aborto, que considerou ter chegado a altura conveniente de divulgar o ponto de vista daquela instituio religiosa. + essa expectativa que poder esboroar-se totalmente se Jernimo e Matos desistirem boca das urnas, j que nesse caso o sucessor de Mrio Soares ser inevitavelmente eleito no dia 14. +Isto porque, de acordo com a lei, nas presidenciais eleito o candidato que obtiver mais de metade dos votos expressos -- ou seja, pelo menos 50 por cento mais um --, excluindo-se desta contabilidade os votos em branco e os nulos. +Nem sempre foi assim. +A questo do peso dos votos em branco no escrutnio final s ficou resolvida com uma alterao da lei eleitoral, de 26 de Novembro de 1985, que veio pr ponto final ao diferendo que ops nessa matria o Supremo Tribunal de Justia (STJ) e o Secretariado Tcnico dos Assuntos para o Processo Eleitoral (STAPE) Comisso Nacional de Eleies (CNE). +Separados por linhas subterrneas, o Metropolitano de Lisboa e a Cmara Municipal andam s avessas. +As solues adoptadas para a expanso da rede esto a gerar posies contrrias e desta vez a empresa que escreve a Sampaio para refutar as afirmaes do Municpio. +07.00-09.00 Cafena. Os ouvintes da Radical acordam com Pedro Marques. +Nono ms do calendrio lunar islmico, durante o qual mil milhes de pessoas se abstm de comer, fumar, ouvir msica ou de ter relaes sexuais, o Ramado o ms sagrado muulmano. + Mas para alguns tambm sinnimo de jihad (guerra santa). +H onze meses, exploses na cidade e massacres em zonas rurais causaram pelo menos 400 mortos e traumatizaram a cidade, onde o grupo islmico armado (GIA) enfrentou, com sucesso, a apertada malha das foras de segurana argelinas. + o que se teme agora, segundo os panfletos, aparecidos sobretudo em mesquitas no controladas pelo Estado, dentro das quais as oraes tm sido acompanhadas por murmrios pouco comuns. +Traumatizado, refugio-me no Canal 1, onde se estreia Tudo pelos Outros, com Vtor Norte a mostrar que estaria muito melhor num palco. +Aguento, com um estoicismo que eu prprio admiro, a inundao de lugares-comuns, comparvel das guas do Tejo, porm menos benfica para a agricultura. +Mas, quando chega o momento de homenagem s mezinhas, no resisto a zapar e a saltar novamente para a SIC. +Perplexidade e desorientao: a SIC tambm transmite homenagens s mezinhas e aos paizinhos, a nica diferena est em que, em vez de flores, do-lhes msica pimba. +Zapo e rezapo e os momentos de sincronismo repetem-se com enlouquecedora constncia: as mesmas lgrimas, as mesmas fungadelas, o mesmo bem-fazer, os mesmos familiares abraados uns aos outros. +J no sei onde estou, talvez j nem saiba quem sou. +Gramm ser o primeiro a declarar formalmente a candidatura, na prxima semana, na sua cidade natal de College Station, Texas. +Na verdade, Gramm planeia a sua campanha h anos e anda activamente na estrada h meses. +O ex-governador do Tennessee Lamar Alexander ser o seguinte a declarar-se, dias depois. +Pouco conhecido fora do Tennessee e de Washington, Alexander conseguiu ganhar a ateno do seu partido ao apresentar sua volta alguns nomes de peso. +Um documento confidencial do Painel Intergovernamental Sobre Mudanas Climticas (IPCC), citado na ltima edio do semanrio britnico Independent on Sunday, afirma que o globo est de facto a aquecer e que a poluio de origem humana um dos factores responsveis pelo fenmeno. +O documento do IPCC vai ser apresentado numa conferncia internacional que ter lugar em Roma em Dezembro. +O IPCC rene nesse documento a opinio de 2.000 meteorologistas que prevem no futuro uma maior incidncia de doenas tropicais, o aumento de secas e cheias, a morte de florestas e a diminuio de colheitas nos pases pobres. +O anterior relatrio do IPCC, elaborado em 1990, considerava que a temperatura do globo estava a aumentar mas no considerava provado que a causa fosse a aco humana. +Agora, dizem os investigadores, j no restam dvidas. +O IPCC um grupo de peritos que foi estabelecido em 1988 pela Organizao Meteorolgica Mundial e pelo Programa das Naes Unidas para o Ambiente. +Mas o Itamaraty tem vindo a sofrer a aco corrosiva dos ltimos governos, que lhe impuseram ministros sem preparao, alm da reduo de mais de 50 por cento do seu oramento. +Os diplomatas brasileiros queixam-se da falta de recursos, j que so obrigados a cobrir muitas despesas com dinheiro do prprio bolso. +Contudo, o Itamaraty apresenta alguns excessos que surpreendem. + o caso das embaixadas situadas nos Estados Unidos e em Londres, Paris e Roma, onde as mordomias saltam aos olhos dos brasileiros. +o diplomata brasileiro vive num apartamento de quase mil metros quadrados na Avenue Foch, junto ao Arco do Triunfo, em Paris. +Ultrapassadas as consideraes gerais, o documento mergulha quase ponto por ponto nas propostas do ministrio sugerindo caminhos, apresentando alternativas e recomendaes. +Desde logo no que toca ao conceito de propina. +Neste ponto, o CNE avana com dois cenrios sobre a controvrsia, revelando como o consenso no seio dos conselheiros no tem sido fcil. +Todos temos de fazer sacrifcios e ns estamos dispostos a colaborar. +Mas -- acrescentou -- receamos que o sacrifcio no esteja a ser repartido de uma forma justa pela populao. +Est a ser pedido aos idosos que se sacrifiquem duas ou trs vezes mais do que o resto das pessoas. +Primeiro, porque vai aumentar os impostos dos beneficirios da Segurana Social. +Segundo porque introduzir mudanas nos programas de assistncia mdica- Medicare e Medicaid- que, ao baixarem os honorrios dos mdicos, far com que estes no queiram assistir tantos doentes, e, terceiro, porque o aumento sobre a energia aumentar os preos do combustvel dos sistemas de aquecimento caseiro, que uma parte considervel dos oramentos dos idosos de baixos rendimentos. +O segmento accionista da Bolsa de Tquio fechou com reduzida alteraes face sesso anterior. +Operadores disseram que o mercado dever manter a mesma tendncia durante as prximas sesses devido forte procura dos investidores domsticos e estrangeiros, tal como vem sucedendo nas ltimas semanas. +O ndice Nikkei perdeu 0,16 por cento. +As audincias mdias de Fera Ferida e Mulheres de Areia permitem tambm analisar o percurso dos noticirios da noite, que lhe esto colados na lgica do programador e nos hbitos do espectador. +o mrito do Telejornal maior se atendermos ao facto de que esse quinto equivale a primeiro ... entre os programas que podem considerar-se fora do pacote de propostas indigentes que continuam a liderar o top). +Os dados de audincias da semana fornecem outras surpresas, de vrio tipo. +Na TV2, a transmisso, em horrio domingueiro de almoo, dos Campeonatos Europeus de Atletismo, hora a que a portuguesa Manuela Machado vencia a maratona feminina, no alcanou mais do que uns mseros 3,0 por cento da populao. +a SIC, que investiu uma nota negra na compra dos direitos de cobertura, e outra nota ainda mais negra nos meios necessrios cobertura, no consegue mais do que um mximo de 5,5 por cento de audincia mdia para as imagens recolhidas por esses meios nunca vistos. +Tanto barulho para nada, parece. +Talvez por, nestes dias, canalizar outros clientes, atrados pelos nomes de David Lynch e Mark Frost, que tambm perderam os seus crditos pela manso de Hugh Heffner. +Uma ltima constatao: a TVI est pulverizada no Top 20 Nacional, sem qualquer proposta acima ou igual aos 10,9 por cento de Szinhos em Casa do Canal 1. +O que, nos tempos que correm, talvez no seja defeito mas feitio. +Ao longo de uma obra original em que tem procurado figurar a gnese e os momentos mais marcantes da nossa modernidade filosfica e esttica, o filsofo Manfred Frank (ainda desconhecido do leitor portugus, mas j muito traduzido em Frana e na Itlia) serve-se, entre outros, do mito do judeu errante, nestas duas vertentes, para seguir um percurso que, na literatura, conduz da viragem para a Idade Moderna e das primeiras viagens pelo desconhecido at contempornea deriva pelos mares da interioridade e condio niilista deste nosso sculo, preo da emancipao crtica em relao s promessas de estabilidade da Razo e de uma modernidade que um dia viria a falar com lngua bfide, afirmando no discurso cientfico o que negava no potico. +As duas vertentes -- Aasvero e o Holands -- cruzam-se e fundem-se a partir do sculo XVI, com um predomnio, no plano da matria diegtica, da deriva martima sobre a errncia terrestre, desde Os Lusadas e as narrativas de viagens inglesas e holandesas, at ao Ancient Mariner de Coleridge, Lgende des Sicles de Victor Hugo, ao Bateau Ivre de Rimbaud e ao Navio Fantasma de Wagner, culminando num episdio-chave da anti-Odisseia que o Ulysses de Joyce: o dos lotfagos (com um antecedente importante no sculo XIX, o longo poema de Alfred Lord Tennyson The Lotos-Eaters), onde Leopold Bloom surge como simbiose do Ulisses polytropos (muito viajado e de muitas manhas), do Judeu errante e do marinheiro Sindbad, comendo do fruto do esquecimento que o levar deriva infinita, para alimentar uma insacivel sede de experincia, sem Itaca que possibilite o reencontro de si. +A Polcia Judiciria de Coimbra deteve um casal suspeito da autoria de vrios crimes de burla, dos quais tero sido vtimas centenas de pessoas. +Ao que tudo indica, o modo de subsistncia do casal consistia na publicao de anncios -- no Jornal de Notcias e no semanrio Expresso -- de emprego fictcios, relacionados com actividades agrcolas no estrangeiro, nomeadamente em Inglaterra. +Era um pneu que s testmos durante trs voltas e corremos o risco de o usar, pensando que era o mais indicado. +Na categoria de 250cc o japons Tetsuya Harada, da Yamaha, alcanou a sua terceira vitria no Mundial -- s perdeu na Malsia --, batendo por mais de quatro segundos o italiano Massimiliano Biaggi (Honda) e o francs Jean-Philippe Ruggia (Aprilia), respectivamente, segundo e terceiro classificados. +A corrida foi completamente dominada por Harada, estreante nesta edio do Campeonato do Mundo, que assumiu o comando partida e nunca mais o largou. +A partir da Zambujeira, as praias favoritas so acessveis atravs dos montes, que se atingem atravessando o ribeiro nas traseiras do caf Fresco. +O Carvalhal a mais conhecida, mas outras h com menos gente e, por isso mesmo, mais escolhidas pelos nudistas. +J quase no limite do concelho de Odemira, aparece a praia da Amlia, mesmo ao lado da polmica Odefrutas de Thierry Russel. +Mais a sul, Odeceixe. +E na Azenha imprescindvel comer um arroz de marisco. +A data de 4 de Novembro tem sido apontada, geralmente, como o limite para o incio da deslocao. +No incio desta semana, no entanto, um diplomata indonsio admitiu a possibilidade da visita comear em finais de Outubro, o que, a confirmar-se, apanhar a Comisso Eventual praticamente no grau zero da preparao da visita. +Na ltima reunio [ quarta-feira passada ], entrmos, tommos uma bica, demos umas palmadas nas costas e viemos embora, porque no havia informaes, revelou ao PBLICO um membro da Comisso. +No s desconhecemos os critrios para os convites imprensa, como ignoramos quem vai e de que partidos, pois o PRD desapareceu da actual AR [ tinha um deputado na Comisso ] e o PCP e o CDS diminuram a representao. +A data aproxima-se, h pequenas coisas a fazer, como por exemplo vacinas, e no podemos estar a vacinar as Cortes inteiras .., acrescentou o deputado, manifestando-se preocupado com o atraso na preparao de uma visita que funciona como um jogo na corda bamba, pois tanto pode ser favorvel a Portugal como Indonsia. +Uma reaco semelhante teve o PCP, por intermdio de Vitor Dias, da sua Comisso Poltica, que salientou que a base da gesto camarria a coligao entre socialistas e comunistas, continuando esta a manter a sua capacidade, lembrando que o que existira com o CDS fora um acordo pontual entre os vereadores centristas e o PS. +Mas, na conferncia de imprensa -- onde estava ladeado pelo seu assessor de imprensa, Antnio Metello --, Jorge Sampaio deixou tambm entender estar para muito prximo uma deciso importante. +A relativa sua recandidatura. +Portugal, com 6,7 por cento dos pobres da CEE. +Um em cada 3 portugueses pobre, uma em cada 3 casas no tem casa de banho e s um em cada 5 jovens sabe utilizar um processador de texto. +Qual ser o pas mais jovem da CEE no sculo XXI? +P. -- Ainda no que respeita ao nvel dos rendimentos, h uma queixa que a oposio e as organizaes socioprofissionais fazem ao Governo e que tem a ver com os custos dos factores de produo, principalmente no que respeita s taxas de juro. +No acha que a actual situao, inserida num contexto fortemente concorrencial, est a penalizar os agricultores portugueses? +R. -- Considero que temos alguns custos de produo mais altos, isso ningum poder negar. +Mas temos outros mais baratos, como os alimentos para os animais e o preo da mo-de-obra. +Sampaio visitou, durante todo o dia, as zonas do Pico e Faial mais atingidas pelo sismo do passado dia 9. +Optou pela pedagogia nos contactos que teve com os sinistrados e nem sequer esqueceu a sua experincia como autarca, recordando inmeras vezes os tempos em que foi presidente da Cmara de Lisboa para explicar as suas teorias -- mais propriamente alguma insatisfao natural -- sobre realojamentos. +Ou pelo menos a forma de evitar alguns conflitos entre a populao. +Foi o prprio Presidente a pedir para reunir com todas as pessoas atingidas pela catstrofe. +Na localidade de Flamengos, onde ainda esto mais de 200 pessoas a dormir em tendas, Sampaio apelou ao bom senso e solidariedade tpica do povo aoreano para compreender que no se podem construir todas as casas ao mesmo tempo. +Quando fui autarca e fazia realojamentos era normal que quem no conseguia logo uma habitao ficasse insatisfeito. +Aqui temos de compreender que no se pode ter tudo ao mesmo tempo. +A soluo, para uma melhor harmonia, que seja a populao a indicar quais so os casos prioritrios, explicou. +Com caixotes do lixo, os alunos finalistas do Instituto Superior de Economia e Gesto de Lisboa (ISEG) fecharam ontem o anexo de Buenos Aires em protesto contra a alterao dos planos curriculares. +Sob o mote 40 cadeiras chegam, os estudantes impediram a entrada no edifcio a partir das sete da manh, tentando assim resolver o problema depois de esgotadas todas as vias diplomticas. +Com os olhos na Rssia, tecnicamente em violao do texto assinado em 1990, os pases-membros do tratado sobre as armas convencionais na Europa voltam a reunir-se a partir de hoje, em Viena, para tentar limpar definitivamente o continente de armas convencionais. +Mas ningum aposta num sucesso fcil. +-- Contraditria. +Ao regime no interessava o desenvolvimento cultural do pas. +O que interessava era beber vinho, beber vinho era de comer a um milho de portugueses, era dar s crianas sopas de cavalo cansado, era ir a Ftima . +-- Ao futebol -- Bom, s para ver o Benfica! +Outra ideia exposta por Soltwedel foi a de que o mercado de trabalho, apesar de envolver pessoas, deve funcionar como qualquer outro mercado, acrescentando que os europeus tm de se livrar da ideia que os salrios no tm nada a ver com o trabalho realizado. +Se no se mudar este curso, os asiticos vo esfregar as mos. +Nesta reflexo sobre o futuro da economia europeia ps-GATT, o professor alemo peremptrio em afirmar que os salrios vo descer, em funo do aumento da competio das empresas, ao mesmo tempo que o desemprego provavelmente crescer ou estabilizar. +Na sua ptica, isto deve-se a todo o tempo em que a indstria europeia esteve protegida. +Depois de tanto tempo debaixo do guarda-chuva, evidente que a Europa se vai molhar. +O Ministrio da Sade vai lanar at ao final de Maio 15 novas experincias na rea dos centros locais de sade, de modo a melhorar a acessibilidade aos cuidados de sade primrios. +O projecto Alfa, assim designado, foi ontem apresentado na extenso de Ferno Ferro do Centro de Sade do Seixal, e passa pela criao de grupos interdisciplinares e pelo alargamento do horrio de funcionamento destes plos de sade. +A importncia do projecto foi realada pela presena em Ferno Ferro do primeiro-ministro e da ministra da Sade. +A titular da pasta da Sade disse que o futuro atendimento nos centros de sade ser mais humanizado e personalizado. + preciso reganhar a confiana das pessoas e incentivar os profissionais, afirmou. +Os bombeiros no conseguiram remover de imediato o corpo, que ficou preso entre as ferragens do conjunto dianteiro de rodas. +Apenas ao meio-dia -- trs horas aps o acidente -- chegou estao Socorro uma carruagem com ferramentas, macacos hidrulicos e cerca de vinte funcionrios da Diviso de Manuteno do Metro. +Os mecnicos levaram ainda uma hora para remover algumas peas do conjunto de rodas, antes de levantar a automotora A-78 e, finalmente, retirar o corpo mutilado de Pedro Alexandre. +Os pais do jovem, depois de ouvirem a notcia do acidente no Metro, j haviam ido s urgncias do hospital So Jos e depois seguiram at entrada do Metro no Martim Moniz. +Aps reconhecerem os documentos do filho, foram conduzidos para a esquadra da PSP da Mouraria, acompanhados da mesma multido de curiosos, aparentemente insatisfeita com a simples apario do corpo numa maca coberta. +Isto parece uma manifestao, comentou um oficial da PSP, antes de ordenar a disperso da aglomerao. +O segundo lugar do torneio repartido pelo ingls D. J. Russel e pelo irlands Ronan Rafferty, ambos com 67 pancadas. +Este, vencedor da ordem de Mrito de 89 e 17 do ranking mundial, posiciona-se agora como o principal favorito vitria no Open portugus. +Embora ainda faltem trs dias de prova, a verdade que o seu momento de forma excelente, alis como o comprovam os resultados alcanados na presente temporada. +O irlands foi primeiro em Palm Mcadows e segundo nos Asian Classic, Dubai Deset Classic e Hong Kong Open. +O seu score no final do primeiro dia do Open, surge na sequncia de 16 voltas abaixo do par. +Um total de 57 pancadas, para uma mdia de trs por volta. +Actualmente quinto classificado na Ordem de Mrito Europeia, o eventual triunfo no evento portugus permitir-lhe-ia ultrapassar Severiano Ballesteros no terceiro lugar da lista europeia dos ganhos monetrios. +Aqui reside a principal curiosidade da prova. +David Silva, o profissional de Vila Sol, por seu lado, foi o melhor portugus em competio, terminando os 18 buracos em Par do campo. +No entanto, comeou mal, fazendo trs bogeys nos trs buracos iniciais. +Seguir-se-ia uma recuperao de grande categoria, na qual alcanou cinco birdies, o que lhe permitiu manter as aspiraes a um lugar entre os 65 finalistas dos dois derradeiros dias da competio. +Se jogar bem amanh [ hoje ], um lugar entre os finalistas inevitvel. +R. -- No tenho sentimentos religiosos, no acredito na vida eterna. +Sei que no h vida do lado de l. +le nant, dizem os franceses. +Tenho dito aos meus que devem preocupar-se com esse dia, no por mim, mas por eles. + uma bonita palavra esta, saudade. +Eu deixarei de existir. +O que ficar de mim a conscincia do que fui e que os outros recordaro -- ou no. +mas se a lembrana for de saudade, ento acho que pode ser uma boa coisa. +Ferreira de Almeida disse ao PBLICO estar a par da situao e que vai solucionar o problema brevemente, colocando uma conduta de cimento ligada ao colector. +Por ora, os moradores desta zona da Ajuda, vivem rodeados de esgotos por todos os lados. +Mas, afirma Vtor Castelinho, a Junta de Freguesia tem um projecto para aproveitar o espao livre que d para a Rua Eduardo Bairrada construndo um pavilho polidesportivo e dois campos de tnis, para servir a populao da Ajuda, espera de luz verde na Cmara. +Vtor Mendes, de 40 anos, licenciado em direito e antigo recordista nacional (2,15m) do salto em altura, desde o ano passado o director da seco de atletismo do Sporting e foi confirmado nas suas funes j pela nova direco. +A sua equipa, encabeada pelos coordenadores tcnicos Bernardo Manuel e Abreu Matos, tambm se mantm, e a tnica antes a do seu reforo, com a entrada de Fernando Mamede para o quadro tcnico, com funes ainda para definir, e de um incondicional do atletismo do clube, Antnio Frade, como seccionista. + um perodo de euforia, em Alvalade, que tem um significado extraordinrio, pois foi o corolrio de um trabalho desenvolvido desde h um ano, como explicou Vtor Mendes. +Quando vim para o Sporting nem havia praticamente equipa de pista formada, houve uma reduo drstica do oramento e estava quase tudo por reconstruir. +Agora os dois ttulos quebraram um jejum de sete e oito anos e foram conquistados, para mais, quando o Benfica era dado como favorito. +S com essa condio aceitei ficar frente da seco. +Ex-primeiro-ministro, de 1974 a 1978, e ex-ministro dos Negcios Estrangeiros da Blgica, no incio da dcada de 80, Leo Tindemans , hoje, o presidente do Grupo Democrata Cristo do Parlamento Europeu, o segundo maior agrupamento parlamentar de Estrasburgo, a seguir aos socialistas. +Europesta convicto, como de resto o so todos os governos deste pequeno pas, dividido ao meio pela lngua e pela cultura, entre um Norte flamengo e um Sul francfono, Tindemans defende que o grau de unio poltica conseguido em Maastricht insuficiente. +Seria necessria uma nova conferncia intergovernamental sobre a Unio Poltica Europeia -- mesmo antes da reviso do Tratado, agendada para 1996 -- e, em seu entender, o alargamento da Comunidade aconselharia a que esta se dotasse de uma verdadeira constituio com metas e com princpios. +Por fim j no conseguia remar mais. +Tinha as mos cobertas de bolhas, as costas queimadas, o corpo doa. +Com um suspiro, mal agitando a gua, deslizei para o mar. +Suficientemente refrescante e estival. +O segundo livro traduzido nesta coleco, deste autor sul-africano. + um primeiro livro e recria a Amrica dos fins dos anos 40 renovando a grande tradio de Hammet e Chandler. +Criou a personagem de um detective negro, Easy Rawling que, no entanto, no procura fazer o contraponto de Marlow e Spade mas, sim, erguer uma figura particularmente convincente num livro contado num ritmo imparvel e com um sabor pungente e autntico. +Segundo o escritor e realizador de cinema Nicholas Meyer era nesta percentagem que Sherlock Holmes dilua a sua cocana, antes de ter sido curado do seu mau hbito pelos amistosos cuidados do dr. Watson. +Mas o optimismo continuou a dominar nas sesses seguintes, apesar das indefinies quanto evoluo das taxas de juro. +O Dow Jones passou novamente a barreira dos 2.900 pontos para se situar pouco acima dos 2906 pontos na sesso de quarta-feira. +Ainda com tendncia positiva, a Bolsa de Nova Iorque evoluiu de forma hesitante at aos 2910 pontos, para no dia seguinte voltar a descer. +Isto apesar de os indicadores econmicos at agora divulgados indicarem um final rpido para a recesso no pas. +Na quinta-feira foi o indicador das encomendas de bens duradouros, que em Abril cresceu cerca 2,9 por cento, melhorando as perspectivas de crescimento industrial. +Na sesso de sexta-feira, ao subir para os 2913,91 pontos (mais 0,45 por cento do que na sesso anterior) o ndice Dow Jones permitiu ganhos da ordem dos 0,95 por cento na semana. +Boris Ieltsin continua a dar sinais contraditrios sobre a forma como vai utilizar a sua vitria poltica no referendo de domingo passado para acelerar o ritmo das reformas e imprimir-lhes uma orientao mais clara. +O cabelo ter ficado grisalho mas o seu bom aspecto e o ar agaiatado no deixam transparecer os seus 50 anos e, muito provavelmente, milhes de fs continuaro a am-lo quando ele tiver 64. +O ex-Beatle Paul McCartney iniciar em breve mais uma extenuante tourne de um ano pelo mundo, para promoo do seu novo lbum. +A campanha para as eleies de dia 19 no Punjab comeou sob o signo das armas e do terror dos separatistas sikhs, que tendem a controlar o curso dos acontecimentos naquele estado indiano. +Ontem foram assassinados cinco militantes do partido Bharatiya Janata, da direita hind. +Ontem, o FC Porto voltou a mostrar fibra de campeo e deu a volta ao jogo com o Martimo, mais uma vez graas ao olho do treinador, que tambm erra, mas acerta mais vezes do que os outros, e a uma equipa que nunca desiste. +Os campees so assim e este, ao cabo de 17 jornadas, leva 11 pontos de avano sobre o segundo, o Guimares, que ontem empatou no Bonfim e j sonha com a Liga dos Campees. +O Rio Ave, outra surpresa, foi derrotado em casa pelo Farense e tem j muitos candidatos nas suas costas dispostos a roubar-lhe o terceiro lugar. +O Estrela da Amadora continua a fazer um campeonato Fernando Santos, ou seja, um campeonato certinho e sem crises, e at venceu o Salgueiros na Reboleira. +O Boavista est a melhorar e empatou no que comea a ser muito complicado estdio do Campomaiorense, numa partida que tambm terminou empatada nas crticas dos treinadores ao rbitro. +O Chaves, com lvaro a treinador, comprou um balo de ar com a vitria em Coimbra. +O Belenenses foi perder ao campo do Varzim e continua a sua marcha a passo acelerado para a Diviso de Honra. +H mais de uma semana que o cessar-fogo entre srvios e muulmanos est a ser mais ou menos respeitado e, semelhana das restantes frentes de combate, a situao nesta regio tem-se mantido calma. +Apesar de em Brcko, 54 quilmetros a noroeste, se terem registado violentos confrontos no fim-de-semana. +Uma espessa sopa de feijo com pedaos de carne, po e beterraba o almoo destribudo no sbado aos soldados srvios. +A refeio subitamente interrompida pela apario de um jipe NIVA 600 de onde saem dois soldados armados -- andar armado nesta regio, seja-se civil ou militar, medida obrigatria -- com os boletins de voto para o referendo dentro de um grande envelope. +Na Videoteca de Lisboa, s 22h30, realiza-se o II Encontro da Associao de Vdeo, Arte e Novas Tecnologias Interactivas PT. +No Centro Cultural da Malaposta inaugurada, tambm no mbito das comemoraes da Revoluo dos Cravos, a mostra colectiva Artistas de Abril, com obras de Joo Vieira, Jos Santa Brbara, Lus Ralha, Maria Keil, Vespeira e Rogrio Ribeiro. +s 21h. +A entidade patronal da Guial, a empresa txtil de Barcelos cujos trabalhadores se mantm em greve desde a passada quinta-feira, requereu ao Tribunal Judicial de Barcelos a ilegalidade da medida tomada recentemente pelos trabalhadores e que consistiu no impedimento da sada de quatro carrinhas da companhia carregadas de mercadoria. +Na sesso de ontem do Mercado Monetrio Interbancrio, que deu incio a um novo perodo de constituio de reservas de caixa, o Banco de Portugal voltou a no anunciar as taxas directoras de interveno e a manter suspensa a facilidade diria, anunciando, no entanto, uma cedncia de fundos at ao montante de 300 milhes de contos, a seis dias, em sistema de leilo de taxa de juro contra a recompra de Bilhetes de Tesouro. +JOS CALADA -- As relaes entre o sindicato e o ministrio no existem. +Esto, em bom rigor, como sempre estiveram. +Desde 22 de Dezembro de 1993 que vimos solicitando senhora ministra uma audincia. +Isto j foi feito por variadssimos faxes, atravs de contactos pessoais em Janeiro de 94, mas no tivemos, at agora, resposta. +Os contactos -- de natureza informal, nem sequer oficiosos -- tm sido com o sr. subsecretrio de Estado-adjunto da ministra. + Em termos estritamente oficiais, este sindicato ainda no conseguiu ser recebido pela ministra, o que no deixa de ser espantoso tendo em conta o tempo decorrido desde a primeira audincia. +P. -- E porqu? +Quanto a Portugal, Ferreira do Amaral reafirmou ontem que as vias prioritrias de ligao Galiza so a concluso da auto-estrada Porto-Valena- e bem assim a concluso do IP1, entre a ponte que ontem inaugurou com Jos Borrell e a ponte que ambos tambm inauguraram h dois anos no Guadiana-, a auto-estrada Famalico-Guimares e o restante trajecto do IC5 at Vila Pouca de Aguiar e da at Chaves e Verin (IP3), e ainda a concluso do IC1 (Porto-Valena) ligando alguns troos dispersos j construdos, como a variante da Pvoa de Varzim e a nova ponte de Viana. +Tudo terminou na Pousada de S. Teotnio, na zona histrica de Valena, entre um matraquear de perguntas dos jornalistas. +Mas teve a compensao de ver, ao lado do seu homlogo, na larga varanda da pousada, os primeiros veculos no oficiais a atravessarem a nova ponte. +E avistou, do lado contrrio, a construo metlica de Eiffel, sucumbida a mais um engarrafamento. +Talvez o ltimo. +Sem entrar em pormenores, devo recordar que as empresas portuguesas a trabalhar na Guin-Bissau rondam a meia centena, das quais seis ultrapassam, em investimentos e responsabilidades, o milho de contos. +Por si ss, aos numerosos pequenos investidores (que no esto includos nos 50) cabe uma fatia de 2,5 milhes de contos. +O Grupo Champalimaud, a Petrogal, a TAP-Air Portugal, a Marconi, a Salvador Caetano, a Tertir, a Somec e a Soares da Costa so algumas das que mais investiram e, presume-se, maiores interesses tm a defender. +Se a isto acrescentarmos o que est a ser perdido -- passageiros e mercadorias na TAP, mercadorias na Portline e na Transinsular, dormidas nos hotis (Hotti e 24 de Setembro), operaes na banca (Banco Internacional da Guin-Bissau e Totta & Aores), turistas nos clubes de caa (como o de Cap) --, as verbas perdidas causaro vertigens. +Quando iam a caminho do Pas Basco, jornalista e operador de cmara receberam a notcia da morte, em Bilbao, de Luiz Andrs Sampiero, um tenente da Guardia Civil encarregue sobretudo de assuntos ligados toxicodependncia. +Esperavam, pois, encontrar uma cidade em estado de stio, mas depararam-se com uma estranha normalidade. +Ainda chegaram a tempo de registar a sada do funeral da Capela do Rei, que no o dos bascos, acompanhado por muitas flores e palmas. +Quando se trata de falar que j mais difcil. +Os bascos no gostam de falar da ETA ou das suas aces. +No falo, choro, declara um transeunte perante as cmaras. + um povo apaixonado, o povo basco. +Mas tambm um povo adormecido pelas atrocidades cometidas pela Espanha federalista contra a individualidade de um povo. +E a maior dessas atrocidades passou-se h 60 anos, em Guernica, para sempre imortalizada no famoso quadro de Pablo Picasso. +Frasto da Silva, ex-ministro da Educao e actual presidente do Instituto Nacional de Administrao, admitiu ter sido sondado para o lugar de Antero Ferreira nos conselhos directivo e de administrao da Fundao das Descobertas, tal como noticiou a edio de ontem do PBLICO. +No falei com o primeiro-ministro, mas fui sondado nesse sentido, disse. +Claro, o tom j outro. +Jacques Chirac fala de terrorismo. +Chama brbaros aos srvios. +Finalmente utiliza a nica linguagem que eles conseguem compreender: a da firmeza. +Fale, senhor presidente! +Procure falar j! +Talvez baste uma palavra mais forte, e que cale fundo, para fazer recuar os fora-da-lei --, trata-se de um progresso que se impe saudar. +Contudo, uma nica pergunta: o novo Presidente ficar-se-por aqui? +Ou ir ao ponto de dizer: mais intolervel ainda que a humilhao de um soldado a humilhao de 300.000 homens, mulheres e crianas, bombardeados quotidianamente, desde h trs anos? +Ele sabe que no lhe dei o meu voto. +Mas tambm sabe que goza de um estado de graa que confere a todas as suas palavras uma imensa repercusso. +Oxal aproveite esta oportunidade. +Oxal seja o primeiro chefe de Estado a tomar finalmente, e de forma clara, o partido da democracia e do direito. +Como gaullista que , seria esta a ocasio histrica para se mostrar fiel ideia da Frana de que se diz herdeiro. +Milo. +Conferncia de imprensa de Jean-Pierre Elkabbach comentando os resultados da cimeira das televises pblicas europeias que reuniu na vspera em Paris. +O que ao certo uma televiso pblica? + E o que a distingue de facto das televises ditas comerciais? +No fundo, e de acordo com Elkabbach, trs apostas: o primado da produo sobre a difuso; a recusa em opor divertimento a cultura; e por fim a vontade de conciliar o imperativo do mercado (o famoso nvel de audincias ao qual definitivamente absurdo pensar que qualquer televiso possa escapar) e a exigncia de qualidade ( qual perfeitamente escandaloso acreditar que uma srie, mesmo popular, deva ser estranha, por natureza). +Trata-se de facto de apostas. +Ou desafios. +Quer sejam formulados com nitidez, quer sejam formulados, sobretudo, medida de essa rede de televises tecida escala do continente -- esta a boa nova da semana. +Parte significativa, mas no integral, do seu reportrio gravado para essa companhia foi reeditado numa compilao temtica de seis LP, primeiro editada pela CBS (actual Sonny) em1986. +Essa mesma colectnea deu mais recentemente origem a um reedio em dois CD duplos, que se encontram esgotados no nosso mercado. +RESPONSVEIS polticos do Imen e da Arbia Saudita tentavam ontem fazer diminuir a tenso entre os dois pases devido a uma velha disputa fronteiria que provocou trs mortos em combates perto de uma ilha contestada do Mar Vermelho. +O vice-primeiro-ministro e ministro dos Negcios Estrangeiros iemenita, Abdel Kader Bajamal, dever deslocar-se hoje Arbia Saudita para tentar desbloquear a querela, disse AFP um diplomata em Sanaa, capital iemenita. +Foi ministro de Jacques Chirac (1986-1988) e agora considerado o seu principal rival, embora ainda no se tenha candidatado oficialmente s presidenciais. +douard Balladur avisou que no tomaria nenhuma deciso antes de Janeiro e acusou Chirac de usar o RPR como uma fortaleza. +As diferenas polticas entre os dois so menos bvias do que o contraste nos estilos. +Chirac, o extrovertido; Balladur, o taciturno. +Balladur nasceu em Esmirna (Turquia), em 1929, e formou-se na Escola Nacional de Administrao, de onde saiu a elite da funo pblica francesa. +Foi colaborador de Pompidou, de 1969 at 1974, depois administrou trs empresas e, em 1986, entrou no parlamento como deputado do RPR. +No Governo, apesar da subida do desemprego, continua a ser popular. +A ltima sondagem d-lhe o apoio de 53 por cento dos franceses, contra 47 para Chirac. +O presidente cessante da Comisso Europeia, Jacques Delors, no se apresentou ainda como o candidato da esquerda sucesso do seu correlegionrio Mitterrand. +Mas ningum duvida de que tem a inteno. +Se no incio as sondagens lhe atribuiam o ltimo lugar, agora mais popular do que Balladur. +Delors, 69 anos, aderiu ao Partido Socialista em 1974. +Antes foi militante da Juventude Operria Crist, lder sindical, funcionrio do Banco de Frana e professor de Gesto na Universidade de Paris-Dauphine. +Foi tambm ministro da Economia e maire de Clichy. +Na CE, quis ser um pioneiro, defendendo o ideal de uma verdadeira federao europeia at ao fim do milnio. +A equipa do Sporting apresentou-se no Municipal de Chaves com Capucho a ocupar a posio de Cadete. +Da substituio no resultou, entretanto, qualquer ganho ofensivo para os lees. +A falta de profundidade atacante, a azelhice no remate -- aspecto em que apenas Juskoviak esteve diferente para melhor -- foram razes para o nulo que se verificava na primeira parte. +Os onze remates do Sporting contra apenas um do Chaves, durante a metade inicial, do bem a ideia da superioridade atacante dos homens de Alvalade e no abonam em nada a capacidade concretizadora dos seus avanados. +Na segunda parte, a tnica do jogo continuou a ser a mesma. +Henrique Calisto decidiu-se, por isso, a mexer na equipa trocando Saavedra por Omer. +Face passividade de Saavedra, o novo treinador do Chaves procurou injectar sangue novo na dianteira, esperando que a sua equipa se tornasse mais agressiva na frente de ataque. +apenas o finlands Tarkki (outra estreia) incomodava, aqui e ali, a defesa sportinguista. +A comercializao e instalao do servio de televiso por cabo em Valongo e Ermesinde da responsabilidade da TV Cabo Porto, uma empresa do grupo TV Cabo Portugal, que prev, at ao fim de 1995, estender este servio a 75 mil habitaes na rea do Grande Porto. +A caravela Boa Esperana chegou doca do Jardim do Tabaco tera-feira, ao entardecer. +Terminava uma rota ocenica de 10 mil milhas nuticas atravs dos portos de Lisboa (Portugal), Cdiz (Espanha), Las Palmas (Canrias), San Juan (Porto Rico), Nova Iorque e Boston (EUA) e Liverpool (Inglaterra) na Regata Colombo 92, uma viagem comemorativa dos 500 anos de descoberta do Novo Mundo. +Foram quase 4 meses de navegao em que 60 velejadores portugueses filiados Aporvela fizeram as vezes de marinheiros sob o comando dos skippers e irmos Joo Lucio Costa Lopes e Jos Incio Costa Lopes. +O estado dos sectores produtivos -- agricultura, indstria --, dos servios e da complexidade da mquina administrativa, bem como a sua articulao com os problemas da pobreza e da modernidade outra das reas. +A pobreza e a Solidariedade assim o tema de um colquio a que Soares assiste em Setbal, no dia 6, onde intrevm o presidente da Cmara, o bispo de Setbal e Bruto da Costa. +Por outro lado, Soares encontra-se com agricultores em A-dos-Ces, no domingo, 31, em Manique do Intendente, na tera-feira seguinte, e ao fim da tarde do mesmo dia, em Vila Franca. +A 5, no Barreiro, visita a Sociedade Agrcola Lavradiense, a 9, na Azueira, Mafra, encontra-se de novo com agricultores, na Central Fruteira e a 10 visita a Adega Regional de Colares. +J no domnio da indstria, inaugura as instalaes da Dan&Cake, em Alverca, e visita uma empresa de flores e o Centro Tecnolgico da Cortia, a 6, no Montijo. +Prefiro no fazer prognsticos pois, como disse ao Presidente [ Felipe Gonzalez ] e ao meu colega, at que recebamos respostas definitivas aos convites de participao j enviados, no gostaria de fazer qualquer anteviso, sublinhou James Baker. +O senhor Baker est quase a conseguir a paz, sentenciou, por seu lado, Gonzalez. +Como o PBLICO noticiou, a Unicre remeteu para a DGCP a fundamentao da nova taxa, onde se refere que os custos do servio, por operao, de 15850. +Um valor bastante superior soma da taxa de cliente, de 100 escudos, com a nova taxa de 30 escudos. +Isto , a Unicre sustenta que os 130 escudos a cobrar diminuem, mas no eliminam os prejuzos do servio. + Pais Antunes, director-geral da Concorrncia e Preos, no ficou inteiramente convencido com os argumentos avanados pela empresa gestora da rede Visa. +E por isso pediu a suspenso preventiva da taxa. +So vrias as dvidas suscitadas DGCP pelo processo da taxa. +Pais Antunes entende, antes de mais, que o modo como a Unicre quer taxar as gasolineiras discriminatrio, na medida em que, em toda a rede Unicre, as bombas so as nicas sujeitas a um valor fixo, e no a uma percentagem sobre o valor das transaces. +Num sector como o dos combustveis, em que as margens de comercializao so esmagadas e sujeitas a preos fixados administrativamente, uma taxa fixa poder, na opinio de Pais Antunes, contribuir para que as companhias se encostem sistematicamente aos preos mximos. +Por outro lado, o director-geral destaca o facto de Portugal ser o nico pas que conhece em que os portadores de Visa esto obrigados a pagar uma taxa de 100 escudos. +Quanto ao peso dos encargos financeiros no valor das vendas, 57 por cento das empresas declara que so inferiores a cinco por cento, ao passo que trs por cento revela encargos superiores a 20 por cento. +Analisando estes dados, a AIP constata que, se um facto que em todos os subconjuntos (empresas industriais, de construo, comrcio e servios, exportadoras e no exportadoras) se verifica uma maioria de empresas em que os encargos financeiros so inferiores a cinco por cento do volume de vendas, bastante significativo o facto de um quarto das empresas apresentar uma relao encargos financeiros-vendas entre os cinco e os dez por cento (29 por cento no caso das industriais) e 18 por cento apresentarem valores superiores a dez por cento. +Com a partida, para terras de outras gentes, de Buddy Guy, o blues ficou mais pobre. +Com o regresso, nossa terra, de Buddy Guy, o blues est mais rico. +Se o seu ltimo disco, Feels Like Rain, j garantia a proximidade da ressurreio, Slippin' In confirma-a. +E, mesmo ao cair da folha, 1994 tornou-se um ano de alegria. +Mestre da guitarra de doze cordas e emblema do blues do Piedmont, Blind Willie McTell um dos guardadores da memria da genuna msica popular da Amrica. +A comear em Outubro de 1929 e acabar em Setembro de 1933, totalizando 41 peas, esta compilao um disco de cabeceira. +A Cmara Municipal de Gaia vai demolir todas as construes de raiz do acampamento cigano de Francelos. + A deciso foi tomada ontem, depois de um encontro com uma representao dos moradores que se queixam do trfico de droga na zona, atribuindo-o comunidade cigana ali instalada h mais de 20 anos. +Esta uma das quatro reivindicaes que a autarquia contemplou para agrado dos moradores que afirmam agora ir aguardar pacificamente a actuao das autoridades. +Paralelamente, circulou tambm na localidade um panfleto apelando ao boicote s aulas nas escolas da zona. +Recorde-se que as mesmas pessoas ameaaram j no votar nas prximas eleies se o problema no for resolvido. +A Polcia Judiciria est a investigar a morte de Jos Antnio Tom, de 39 anos, cujo corpo foi encontrado, ontem de madrugada, em Troves, S. Joo da Pesqueira. +Ramos Lopes, um histrico do PSD local, que foi presidente da primeira concelhia e actualmente director da Fundao Gulbenkian, encabea uma lista sem nomes sonantes da actual gesto autrquica, que integra, entre administradores de empresas, o escultor scar Guimares e Maria Joo Vieira, assessora do ex-ministro Diamantino Duro. +Aos 15 anos, Pedro, Meloman, Sapito para o people da Arrentela e do Monte da Caparica, j fora acusado e absolvido de assalto a um estabelecimento. +J passara muitas noites nas esquadras da PSP e nos postos da GNR e j apanhara muita porrada da bfia. +Mais qualquer discurso fez que no recordo. +Mas mais espantoso ainda que nos esperou depois porta de sada, no para cumprimentar os clientes -- como de bom tom! --, mas de novo agredir verbalmente o meu anfitrio, dizendo-lhe No volte c mais! ... e atirando-lhe com a porta na cara. +No pretendo fazer ironia nem obter resultados. +mamem a pastilha elstica caladinhos ... seno rua !!! +no precisa de se preocupar. +Mas no lhe ensinaram aquela antiga regra do negcio que diz que um cliente descontente um cliente perdido? +E at pode no saber quem ... +s estava presente o aoreano. +Nos vinhos a coisa melhorava um pouco: uns borbas e uns reguengos razoveis, um Paulo da Silva (Colares) e alguns Bairrada, vinho e espumante. +Azeite, creme de castanha, frutos secos e algumas ovelhas concluam o que Portugal tinha para mostrar. +Foi pouco. +Depois de trs dias a visitar a feira as opinies dividiam-se um pouco, sobretudo entre professores e alunos. +Os primeiros estavam desencantados com o facto do Salo deste ano no ostentar o brilho, nem as dimenses, de edies anteriores. +Foi um dos primeiros sintomas do efeito que as decises de Maastricht tero na ultra-subsidiada agricultura francesa -- e o primeiro sinal de retrao. +Vitimado por um ataque cardaco, morreu no passado dia 3 de Julho, em Nova Iorque, o guitarrista Johnny Copeland, um dos ltimos nomes carismticos do blues texano. +A notcia da sua morte s ontem foi conhecida, atravs da revista americana Variety. +A maior rea queimada verificou-se no distrito de Santarm, com oito mil hectares, enquanto em Bragana arderam apenas 64 hectares. +No perodo de 1 de Junho a 7 de Julho os bombeiros foram chamados a 7249 fogos florestais. +Entretanto, o violento incndio que deflagrou tera-feira no concelho do Fundo, entre as povoaes de Barco e Lavacolhos, voltou a reacender-se na madrugada de ontem, aps ter sido considerado extinto na quarta-feira, segundo informao dos bombeiros locais. +Ontem, discursando no Washington Institute for Near East Policy, no mbito da sua terceira visita oficial aos EUA, Netanyahu voltou a propor o reatamento do dilogo com a Sria sem condies prvias. +Isto , ignorando tudo o que foi negociado nos ltimos cinco anos, o que Damasco considera inaceitvel. +Netanyahu garantiu ao Presidente srio, Hafez Assad, que encontrar nos israelitas parceiros razoveis e cooperantes se escolher o caminho da paz. +O problema que, para Netanyahu, esse caminho passa primeiro pelo fim dos ataques do Hezbollah no Lbano, enquanto para Assad passa primeiro pela devoluo dos Gol. +Ns dizemos que queremos retirar-nos do Lbano, mas na imprensa sria respondem-me: no se retirem, queixou-se Netanyahu. +A Sria procura ostensivamente a nossa retirada mas, na prtica, impede-a. +Para os actores uma teraputica, para os espectadores uma aprendizagem e uma lio. +Os oitos actores do Grupo Teatro Teraputico do Hospital Jlio de Matos entregam-se com tal verdade e liberdade, falam to desassombradamente de temas-tabus, que os espectadores se sentem em desvantagem. +Quem so os doentes: eles ou ns? +Rua do Ouro: reparado at hoje. +Rua da Prata: reparado at hoje. +... ela atende a Academia Sueca ... +Ele, em Frankfurt, ela em casa. +Foi 15 minutos antes do anncio. +Eu estava na cozinha, onde quase tudo de importante acontece naquela casa, e telefonaram-me. +Atendi e creio que falou uma senhora em ingls -- eu no sei ingls --mas quando ouvi ' Academia Sueca ' no ouvi mais nada, fez-se um vazio na cabea, no sei se agradeci, se fui corts, se dei um grito. +Porque toda a gente ia para o aeroporto de Madrid espera dele, era o que estava previsto. +A minha preocupao era essa, ter mandado toda a gente para o aeroporto ... +Quando lhe perguntamos quais so as possveis razes desta ascenso aparentemente irresistvel das mulheres, Whipp responde-nos que acha que existem dois factores principais. +Talvez no seja de estranhar, alis, que as novas campes da corrida sejam originrias da China, o pas mais populoso do mundo -- onde o universo da escolha potencialmente maior do que em qualquer outro pas do Mundo. +O ritmo de treino das novas campes chinesas, por exemplo, absolutamente espectacular. +Entre outras coisas, elas correm uma maratona por dia em terreno acidentado -- mais de mil quilmetros por ms! -- e durante cinco a seis vezes por ano o seu treino decorre na meseta tibetana, a cerca de cinco mil metros de altitude. +Os etopes esto este ano ausentes da pista das Aoteias, o que retira algum fulgor prova masculina. +Na corrida feminina, o nvel superior, pese embora a falta da americana Lynn Jennings, a tricampe mundial de crosse. +... e municipais na Tunsia. +Quanto Federao Nacional de Professores, que ser recebida na prxima tera-feira pelo Presidente da Repblica, a quem vai falar sobre a situao no sector da educao, mantm um dos dois dias de greve marcados durante o mandato de Diamantino Duro. +Os dirigentes da Fenprof avisam no entanto desde j que se Couto dos Santos insistir nalgumas das directrizes dos seus antecessores arranjar lenha para se queimar. +A mudana de ministro no basta, diz a Fenprof, disposta a mostrar o descontentamento dos professores na manifestao do prximo dia 27, data da greve. +Depois de uma anterior fase de conversaes, terminada h 11 dias, a Itlia conseguiu persuadir a comunidade armnia do Nagorno-Karabakh a deixar de boicotar as negociaes de Roma. +Mas depois da nova ofensiva azerbaijana, com centenas de tanques, helicpteros e avies de ataque, os dirigentes armnios do enclave disseram que no conseguiam sair de l. +As conversaes de ontem, presididas por Mario Raffaelli, o mesmo que dirige o processo de paz moambicano, comearam sem eles, embora a prpria Armnia tenha estado presente. +Mantendo uma relao evidente com a estrutura histrica dos encontros, Albano da Silva Pereira, organizador principal, articula as escolhas entre o nacional e o internacional, a consagrao e a revelao, o pedaggico e o experimental. +por outro, corre riscos estticos. +Sendo a fotografia um lugar de registo dos corpos, parece que, este ano, do conjunto de exposies se desprende mais uma atitude de registo, a participao do fotgrafo na construo e seleco da imagem do que a afirmao das prprias imagens, do seu corpo. +estabelecer uma ordem na multiplicidade catica das imagens pode ser uma tarefa exterior e desligada desse real mas tambm pode, ao procurar entender as intenes dos fotgrafos, enriquecer o conjunto fornecer-lhe um novo sentido. +Foi este homem, marginalizado at em decises que diziam respeito ao seu pelouro, que em Janeiro decidiu partir a loia. +No queria ver o seu nome ligado a uma gesto altamente duvidosa e pediu ao secretrio de Estado dos Mercados Agrcolas que mandasse investigar as irregularidades de que tinha conhecimento. +No essencial, tratava-se da adjudicao de servios de promoo de imagem e de obras sem concurso pblico nem visto do tribunal de contas (ver PBLICO de 21 de Abril). +Chegadas Comisso Parlamentar de Agricultura atravs de uma notcia do PBLICO, as acusaes em questo levaram os deputados a ouvir Pedro Rodrigues, agora na prateleira do Iroma, e Branco Rodrigues, agora presidente do Conselho de Administrao da PEC-Alimentao, a empresa que controla as quatro PEC regionais entretanto criadas. +Concluda a audio parlamentar, o deputado comunista Lino de Carvalho, relator do processo, no teve dvidas em propor que a Comisso Parlamentar assumisse a iniciativa de inqurito s irregularidades e ilegalidades detectadas no captulo da promoo da imagem das PEC e da publicidade da sua privatizao. +O PS anuu de imediato e o PSD, atravs do seu coordenador para a rea da Agricultura, Carlos Duarte, admitiu claramente a hiptese de vir a subscrever a proposta de Lino de Carvalho. +Depois de ouvirmos o depoimento do eng. Pedro Rodrigues ficmos com algumas dvidas relativas aos concursos pblicos e penso que h aqui algumas questes que o prprio Governo no conhecia, afirmou Carlos Duarte ao PBLICO h duas semanas. +Anteontem, porm, o PSD rejeitou a proposta de inqurito pretextando com o facto de estar em curso uma inspeco da iniciativa do prprio Ministrio da Agricultura. +Escrevemos ao PS pedindo um envio rpido da contra-proposta, passadas trs semanas ainda no conhecemos oficialmente a posio do PS. +Procurei na ltima semana contactar o presidente da concelhia socialista, mas, encontrava-se no estrangeiro, espero vir a concretizar esse contacto ainda esta semana -- conclui Carlos Arrojado. +As celebraes do tricentenrio da morte do (assim convencionado) primeiro heri nacional do Brasil, Zombi dos Palmares, tem destaque especial na edio de hoje do Acontece, na TV2. +mais de 300 mil cpias vendidas desde o Natal, shows continuamente esgotados no Caneco, por onde passaram j cerca de 50 mil pessoas, recepo calorosa por parte do pblico. +O regresso s memrias do passado coloca nos olhos de Joaquim Afonso um brilho de indisfarvel saudade. +Dantes, a vida era cheia de felicidade. +A aldeia tinha mais liberdade, garante. +A agricultura foi, desde pequeno, a sua nica profisso. +Mas fiz o exame da quarta classe com distino, assegura, com evidente orgulho. +Tem razes para isso, j que as aulas exigiam-lhe diariamente um sacrifcio de mais de 22 quilmetros, feitos a p, qualquer que fosse a poca do ano. + medida que vai desfiando parte do seu passado, Joaquim Afonso olha fugazmente o cu, abre mais os olhos, esboa um sorriso e l vai continuando a sua conversa. +Desta vez, para lembrar uma prenda do seu pai, que, num dos invernos rigorosos de Busteliberne, resolveu fazer-lhe uma surpresa, oferecendo-lhe umas botas. + Eu gostava tanto delas que, quando passava em stios onde sabia que no encontrava ningum, tirava-as e ia descalo. + Para as poupar, justifica. +A cotao de Lenine nunca foi to baixa. +As filas gigantescas entrada do imponente mausolu na Praa Vermelha desapareceram. +Os russos preferem agora passar horas numa fila para um smbolo do capitalismo: o McDonald's e os seus hamburgers. +O prximo czar da Rssia vai ser colocado no trono pelo dinheiro, proclama Alexandre Melnik, um diplomata russo. +As crianas que abandonam a escola na ilha de So Jorge, Aores, para ajudar os pais e a economia familiar so ainda em grande nmero, segundo informou o presidente da Comisso de Proteco de Menores de Velas. +Carlos Noysan acrescentou que o absentismo escolar constitui o principal motivo de queixas apresentadas comisso. +Entre as razes apontadas pelo mesmo responsvel para a fuga escolaridade obrigatria, em So Jorge, esto dificuldades no pagamento do almoo das crianas nas cantinas das escolas, a falta de dinheiro para a compra de livros e a carncia de transportes. +Problemas que, de acordo com Carlos Noysan, o Instituto de Aco Social e o Centro de Prestaes Pecunirias da Segurana Social tm procurado solucionar. +O Governo Regional dos Aores comprometeu-se a pagar os juros de um emprstimo a contrair pela Cmara de Santa Cruz da Graciosa e empresrios locais, visando a compra de um barco de passageiros orado em 54 mil contos. +A embarcao, de 30 lugares e 14 metros de comprimento, destina-se ao trfego entre as ilhas do grupo central do arquiplago, viajando prioritariamente entre a Graciosa e a Terceira. +O secretrio regional da Economia, Duarte Ponte, justificou o apoio governamental com o envolvimento camarrio no projecto, desafiando outras autarquias das ilhas a iniciativas semelhantes. +A partir de Maio, outra embarcao com capacidade para 150 passageiros ser alugada pelo governo regional para ligar as ilhas dos grupos central e oriental. +Para o desempenho das suas atribuies especficas no campo social, a Santa Casa da Misericrdia de Lisboa (SCML) assegura a obteno de meios financeiros prprios atravs, sobretudo, da organizao e gesto, a nvel nacional, da lotaria nacional, das apostas mtuas (totobola, totoloto e joker) e, agora, tambm, da lotaria instantnea, em cujos lucros comparticipa. +Foi D. Maria I que, por real decreto assinado a 18 de Novembro de 1783, concedeu a lotaria Santa Casa da Misericrdia, com o objectivo primordial de sustentar os hospitais reais de enfermos e expostos. +A medida acabaria por imprimir ao jogo uma credibilidade que, de outro modo, talvez no tivesse. +Actualmente, de acordo com a lei vigente, a SCML recebe um tero dos lucros lquidos da lotaria e o Estado fica com dois teros. +Em Monte-O-Novo feriado municipal. +s 15h00 sai rua a procisso em honra de S. Joo de Deus. +Uma hora mais tarde o Movimento Democrtico de Mulheres promove um convvio no ginsio municipal. +No Cine-Teatro Curvo Semedo, Mafalda Veiga, Francisco Fanhais, entre outros, participam, s 21h00, num espectculo produzido pelo grupo Porta Aberta. +O grupo de teatro Maizum apresenta a pea Florbela. +Silvina Pereira interpreta o papel da poetisa Florbela Espanca. +s 17h00, no Museu Nacional de Arte Antiga, s Janelas Verdes. +No lado oposto desta realidade esto o Barcelona e o Real Madrid. +Os catales, em baixa de forma que no poupa mesmo Figo, suavizam as suas prestaes com o ataque mais realizador -- 37 golos -- e o terceiro posto. +Lugar de pdio, mas a sete pontos do comandante e a dois do rival local, o Espanyol, outra das revelaes. +Est longe a constituio no Nou Camp de um novo dream team, como o de Romrio e Stoichkov, o que j enerva a direco. +Consequncia imediata: as diatribes do tcnico Johan Cruyff, consentidas com os xitos, apresentadas mesmo como sinais de genialidade, passam agora a ser criticadas. +J no Real Madrid, apesar de a equipa continuar mal -- foi humilhada pelo Deportivo da Corunha na quinta-feira com trs golos de velocidade de Bebeto --, de no reencontrar a frescura que a levou na poca passada ao ttulo, o treinador Jorge Valdano continua com o apoio da direco. +Adivinha-se que a prazo, enquanto as dvidas do clube concentrarem a ateno dos dirigentes e as contas no possibilitarem o pagamento da resciso do contrato. +No de literatura do que aqui se trata, embora a histria, densa e trgica, nos envolva desde a primeira pgina. +Uma histria bem real, to real e violenta que o livro foi j posto fora de circulao oficial no Brasil, tal as ondas de choque que provocou o seu testemunho-denncia, vinte anos j passados sobre o desaparecimento sem rasto de Snia Maria de Moraes Angel Jones. +Os Estados Unidos devem subir substancialmente as taxa de juro durante o prximo ano, de forma a manter a inflao sob controlo e manter um ritmo de expanso sustentada da economia. +O conselho foi ontem dado pelos tcnicos da OCDE, no seu mais recente relatrio sobre o estado da economia norte-americana, em que se avisa as autoridades do pas a reduzirem os gastos com a Segurana Social com o objectivo de assegurar, a longo prazo, a sade financeira federal. +A organizao prev que os Estados Unidos cresam 2,9 por cento em 1995, contra 3,8 por cento este ano, enquanto a inflao passar de 2,1 por cento em 1994 para 2,8 por cento no prximo ano. +A OCDE considera que nos dois ltimos anos a economia norte-americana teve um bom desempenho, mas receia o aumento das tenses inflacionistas e novas quedas do dlar, caso as taxas de juro no venham a ser aumentadas. +Ao que tudo indica, a deciso de uma nova reunio com o ministro foi j tomada, embora a data ainda no tenha sido estabelecida. +Enquanto isso, os universitrios do Norte tm estado a reunir-se e a estudar cada um dos princpios apresentados pelo titular da pasta da Educao. + o caso do ncleo do Porto do Movimento Nacional Contra o Aumento de Propinas e da Associao de Estudantes da Universidade de Aveiro, que tm estado a discutir as mesmas propostas com vista a elaborar um documento onde seja patente a sua tomada de posio sobre o assunto. +Para Miguel Dias, dirigente estudantil da Universidade de Aveiro, as palavras de Couto dos Santos no Porto deixaram muitas dvidas em relao justia social. +Concretizando, aquele universitrio referiu a convico de que no devem ser os estudantes a pagar pelos erros de certos reitores que no tm feito bem a gesto dos dinheiros. +A inteno dos estudantes de Aveiro , agora, saber como feita a gesto das universidades e, por outro lado, saber qual o fim do aumento das propinas. +Para os habitantes do Sul, observou a Reuter, a discusso da guerra civil nas Naes Unidas d credibilidade ao seu Estado, embora este seja apenas reconhecido pela Somalilndia, outro territrio secessionista, da vizinha Somlia, que tambm ningum reconheceu. +H dois tipos de reconhecimento na lei: um claro e outro implcito, comentou Abdul-Moneim Abdullah, professor de Direito na Universidade de den. +Por enquanto, estamos felizes s com o reconhecimento implcito. +Enquadrada no local mais terciarizado da capital, a exposio Habitao Lisboa/92, organizada pela autarquia no Terreiro do Pao, pretende, durante a semana de 16 a 23, no s lanar o debate sobre os problemas no sector como relembrar a fuga de habitantes para fora da cidade, substitudos pelos servios e o comrcio. +Com excepo para o Instituto Nacional da Habitao e do Instituto de Gesto e Alienao do Patrimnio Habitacional do Estado (os dois organismos estatais ligados ao sector) -- que declinaram o convite feito pela Cmara -- estaro presentes no certame diversas entidades com responsabilidades na Habitao. +Assim, entre os 28 expositores contam-se as autarquias, as empresas construtoras, os bancos, as imobilirias, as seguradoras e as associaes como a dos inquilinos e a dos proprietrios, entre outras. +11 de Setembro -- O Bundesbank intervm em defesa da lira, sendo seguido pelo banco central da Blgica. +13 de Setembro -- O Comit Monetrio da Comunidade opta por um realinhamento do SME, com a desvalorizao da lira italiana em 3,5 por cento e a valorizao das restantes divisas do Sistema em 3,5 por cento. + Carlos do Carmo interpreta temas capella, outros em que acompanhado por apenas um instrumento e apresenta uma cano indita. + o primeiro concerto do programa Vozes. + Espectculo do Ballet Folclrico Mexicano, no mbito da programao oficial do pas. +Um ms aps a Declarao anglo-irlandesa sobre o futuro do Ulster, continuam longnquas as hipteses do Sinn Fein, o brao poltico do IRA vir a aprovar os seus termos. +O documento pouco favorvel aos republicanos que s na Pscoa se pronunciaram oficialmente sobre ele. +Interessa antes de mais decidir quem preferimos para representar a Repblica, a que somos e a que queremos vir a ser. +A representao depende muito da qualidade do ser do representante. +A este nvel o estilo o homem. +Nenhuma dvida me sobra ao comparar Cavaco, smbolo de um economicismo rasteiro e de uma extrema escassez cultural e espiritual, com Sampaio, pessoa de outra espessura humana e promessa de que a poltica continuar a predominar em Belm. +E quem no sentir que o vnculo entre entre Sampaio e as liberdades polticas no s mais antigo mas mais visceral? +A agncia Nova China informou que para redigir este dicionrio de 34 470 entradas em lngua chinesa, foi necessrio o trabalho de 300 especialistas durante trs anos. +A enciclopdia considerada o primeiro grande instrumento de trabalho exaustivo e sistemtico para o mestudo do marxismo-leninismo a ser publicado depois do nascimento da doutrina marxista, explicou a agncia. +A primeira edio, de 11 mil ecxemplares, est j reservada na sua totalidade. +A FRENTE Polisrio acusou ontem Marrocos de violar pela terceira vez o cessar-fogo no Sara Ocidental ao enviar avies para sobrevoar a povoao de Mijek, no sudeste do territrio. +Para que possa no responder s violaes marroquinas, a parte sarau exige que a comunidade internacional lance um alerta a Marrocos para que cesse as provocaes e para que se comnporte de forma responsvel, respeitando os seus compromissos, declarou a Polisrio, organizao que luta pela independncia do territrio do Sara Ocidental, num comunicado divulgado em Argel. +O norte-americano Pete Sampras foi afastado pelo seu compatriota Jim Courier (24 ATP) pelos parciais de 7-6 (7-5), 6-4, o que significa que o nmero um do mundo vai chegar catedral da terra batida, Roland Garros, com duas derrotas em outros tantos encontros disputados sobre o p de tijolo. +Michael Chang, nmero dois do ranking, foi eliminado pelo argentino Hernan Gumy (54 ATP) com os parciais de 6-3, 6-2. +Surpreendentes foram tambm as derrotas dos finalistas do torneio de Hamburgo, na semana passada, ambos batidos por australianos. +Andrei Medvedev, vencedor do torneio alemo, foi afastado por Scott Draper por 7-5, 6-3, e Flix Mantilla (cabea de srie n13) foi eliminado por Patrick Rafter por 6-1, 3-6 e 6-4. +Quando a advogada lhe perguntou porque que o carro dos portugueses no teria sido interceptado, Cardiell afirmou que pensava estarem combinados com a polcia, tendo em conta a forma como as coisas se passaram. +Se nos tinham localizado, podiam ter actuado quando ainda estvamos com os portugueses, afirmou. +A polcia no quer que eu preste declaraes. +No lhes interessa saber mais dados. +V-se que este trabalho foi preparado pela polcia, que ter facilitado a entrega da droga para nos deter, rematou o arguido, sugerindo que pode perfeitamente identificar os portugueses envolvidos se as autoridades mostrarem interesse nisso. +Para um utilizador experiente ou para um principiante, uma ligao rpida, eficaz e econmica Internet em Portugal s assegurada atravs da Teleweb ou da Esotrica, dois dos cinco fornecedores que operam em Portugal. +Esta a concluso a que chegou a revista de consumidores Pro Teste, que publicou na sua edio de Setembro um estudo sobre esta matria. +Fora das fronteiras do Imprio, consegue pouco sucesso no Oriente, mas seduz os germanos atravs da heresia do arianismo, que considerava Jesus apenas como um homem, excluindo a sua dimenso divina. +Mais tarde, os povos que aderiram e esta derivao sero duramente reprimidos. +Inicialmente religio dos pobres, a nova mensagem depressa se estende a todas as camadas sociais e ser vigorosamente divulgada pelos pequenos grupos iniciais, que depois do origem a novas comunidades. +A devoo ou o arrependimento de pessoas ricas implica, em simultneo, a concesso de doaes s autoridades crists, que as enriquecem. +Trezentos anos aps a morte de Jesus, o cristianismo iria tornar-se a religio oficial do Imprio. + Um grupo autodenominado Combatentes pela Liberdade do Lbano reivindicou ontem o rapto de um oficial da Fora Area norte-americana e do seu filho, que desapareceram na Turquia, e ameaou execut-los caso no seja libertado um dirigente do Hezbollah, revelou ontem em Ancara a agncia Anatolia. +A agncia referiu que um indivduo no identificado e que se exprimia mal em turco telefonou de um pas estrangeiro para afirmar que o grupo tinha em seu poder o tenente-coronel Mike Couillard, 37 anos, e o seu filho Matthew, dez anos. +Os dois norte-americanos desapareceram h trs dias no Ocidente da Turquia. +Segundo apurou o PBLICO, o ministro Ferreira do Amaral nomeou o presidente do Conselho Superior de Obras Pblicas, Armnio Faria, para representar o MOPTC nas negociaes tendentes a um acordo. +Um gesto que revela disponibilidade do Governo para um acordo de cavalheiros que o presidente da Cmara de Fafe, Parcdio Summavielle, classifica de francamente positivo. + A histria remonta a 1983, altura em que o Governo do Bloco Central, atravs do Ministrio do Equipamento Social, titulado pelo socialista Rosado Correia, assinou com a Cmara de Fafe um protocolo no qual se comprometia a custear um centro coordenador de transportes at ao montante de cem mil contos e a financiar em 90 por cento a construo de uma via circular cidade, como contrapartidas desactivao da linha frrea entre Guimares e Fafe. +O XVIII Festival Internacional de Msica da Pvoa de Varzim abre hoje a sua programao com um concerto por o Coral de Letras da Universidade do Porto e da Orquestra Esproarte, s 21h30, na Igreja Matriz da cidade. +O espectculo conta com a participao de Rui Taveira (tenor), Oliveira Lopes (bartono), Thomas Gal (piano), Isabel S (harpa) e Helena Sofia Pereira (tmpanos). +Os msicos iro interpretar, sob a direco do professor Jos Lus Borges Coelho, a cantata BWV 4 de J. S. Bach, bem como a Sinfonia Simples e a cantata Misericordium, ambas de Benjamin Britten. +Em Paos de Brando inicia-se tambm o XIX Festival de Msica de Vero. +Na sede do Crculo de Recreio, Arte e Cultura actua, pelas 21h45, o Quarteto Lrico do Real Teatro de Queluz, que formado por Elsa Saque, Carlos Guilherme, Ana Ferraz e Wagner Dinis. +Os cantores sero acompanhados ao piano por Armando Vidal. +Hoje, a ltima oportunidade de assistir ao espectculo de dana clssica indiana, com coreografia e interpretao de Mallika Sarabhai, uma das mais eminentes bailarinas indianas, especializada nos estilos Bharata Natyam e Kuchipudi, e cuja participao na obra de Peter Brook, Mahabharata, no papel de Draupadi, lhe concedeu fama internacional. +O treinador portista optou por uma marcao individual e muito atenta sobre os homens do Corunha. +E logo aos 4', as cerca de 500 pessoas que se deslocaram ao Pavilho das Antas assistiram ao primeiro golo apontado por Pedro Alves. +Durante a primeira parte, a constante rotao defensiva operada pelos portistas conseguiu anular a rapidez dos espanhis. +A concentrao defensiva, e consequente nmero de bolas recuperadas, viria a originar o segundo e terceiro golos portistas, ainda na primeira parte. +Primeiro por Pedro Alves, num inesperado remate meia volta, e a dois minutos do intervalo por T Neves que, de costas, conclui uma bonita jogada de envolvimento. +Para terminar: Francisco Paula de Oliveira merece hoje o protagonismo e a importncia que lhe foi negado. +O reconhecimento que tarda. +O respeito que se impe. +Os New Jersey Nets perderam a invencibilidade no campeonato, pois foram derrotados pelos Chicago Bulls, por 99-86. +Os Bulls, com 21 pontos de Steve Kerr, regressaram s vitrias e somam agora quatro triunfos -- todos eles conquistados em casa -- e duas derrotas, que lhe do o terceiro lugar na Diviso Central. +Apesar da derrota, a equipa de New Jersey pode orgulhar-se de ter obtido, nesta poca, a melhor srie vitoriosa num incio de campeonato (quatro triunfos consecutivos) desde 1976, altura em que se estreou na NBA. +A privatizao do BFE processar-se- por concurso pblico de 65 por cento do capital do banco. +O preo mnimo por aco de 1980 escudos, valor que se encontra prximo da oferta pblica de aquisio final feita pelo Banco Portugus de Investimento, em Fevereiro, o que atribui ao BF um valor global de 158,5 milhes de contos. +A operao de venda, que incide sobre 52 milhes de aces, permitir ao Estado um encaixe mnimo de 103 milhes de contos, destinado ao Fundo de Regularizao da Dvida Pblica. +O concurso pblico para a alienao do BFE aberto a investidores, individualmente ou em grupo, que observem os requisitos de dimenso de activos, fundos prprios e capitais, estipulados no caderno de encargos, a aprovar por Resoluo do Governo. +Stuart Eizenstat, sub-secretrio de Estado do Comercio norte-americano, lanou um apelo s autoridades do Estado da Califrnia para que reveja a sua deciso de boicote aos bancos suos, aparentemente sem efeito. +O Governo suo reagiu criticamente a este boicote, ameaando recorrer Organizao Mundial do Comrcio (OMC). +A tenso entre os EUA e a Sua vai seguramente crescer com a entrevista dada ao L'Hebdo, de Genebra, pelo venervel universitrio Jean-Franois Bergier e publicada quarta-feira. +A Oliva informa os obrigacionistas da emisso Oliva/89 que o valor lquido do juro por obrigao de 34 escudos e est disponvel a partir de 20 de Junho na sede da empresa. +A Indelma-Indstrias Electro-Mecnicas SA estar cotada durante 30 dias no mercado sem cotaes. +A permanncia no mercado comea a ter efeito a partir do dia 31 de Maio. + O ANTIGO chefe de Estado do Uruguai, Julio Sanguinetti, foi eleito para a Presidncia da Repblica nas eleies gerais de domingo, segundo a projeco de um instituto privado aps o escrutnio de 15 por cento dos votos. +Segundo o instituto Cifra, o Partido do Colorado (liberal), de Sanguinetti, ter vencido a corrida eleitoral com 33,5 por cento dos sufrgios, contra 31 por cento do Partido Nacional (conservador), do Presidente cessante Lus Alberto Lacalle, e 30 por cento da Frente Ampla (esquerda). +Sanguinetti, um advogado de 58 anos, que dirigiu pela primeira vez o pas entre 1985 e 1990, ficar no poder por um mandato de cinco anos. + Arrigo Sacchi, seleccionador nacional italiano de futebol, indicou ontem os 22 jogadores que vo representar o pas no Mundial de Futebol dos Estados Unidos. +Os atacantes Gianluca Vialli e Gianluigi Lentini ficaram de fora. +O Milan -- finalista, com o Barcelona, da Liga dos Campees Europeus -- o clube que mais jogadores fornece seleco transalpina, com sete futebolistas, seguindo-se o Parma, com cinco, a Juventus e o Lzio de Roma, ambos com trs. +Na manh seguinte, com surpresa minha, o telefone tocou e falei tudo o que era necessrio. +Espantada com o facto, resolvi aproveitar e fazer algumas chamadas que se encontravam pendentes h dois dias. +S consegui fazer duas, terceira tudo voltou ao princpio. +Chegada ao meu local de trabalho, tentei desta vez falar com a assistente do meu nmero de telefone. +Finalmente algum me entendeu, pois de imediato me disse que deveria tratar-se de ... uma avaria na central! +Como fiquei contente por, ao fim de dois dias, ter conseguido fazer-me entender pela Portugal Telecom. +E confesso que a minha lngua-me o portugus ... +Frei Bento Domingues, O.P. +Diz-se que um documento do Vaticano ou provoca uma grande polmica -- e torna-se um sucesso editorial -- ou vai dormir tranquilamente para as bibliotecas eclesisticas. +Joo XXIII teria sido a santssima excepo. +Frederico Cunha celebrar o seu 48 aniversrio no prximo domingo, na companhia dos seus familiares no Brasil. +Distante, com a pena de 15 meses de priso suspensa, pela infraco de favorecimento pessoal, o seu afilhado Jos Miguel Noite completar 28 anos no prximo dia 24, num pas da Unio Europeia onde estuda como bolseiro de uma instituio madeirense. +Os mercados de aces fecharam ontem em alta, com as bolsas de Londres e Paris (Frankfurt esteve encerrada) a beneficiarem de uma onda de compras coincidente com o comeo da cimeira do Grupo dos Sete (G7) pases mais ricos do mundo, em Halifax, Canad. +O dlar subiu para valores acima dos 1,41 marcos, aproximando-se cautelosamente dos 85 ienes e mostrando grande firmeza. +Um dlar estvel ajudou os investidores activos nos mercados de aces, na medida em que serve de suporte aos exportadores europeus, disseram operadores. +O ndice FTSE-100 da Bolsa de Londres subiu 0,92 por cento, enquanto o CAC-40, de Paris, valorizou-se 1,43 por cento. +A Bolsa de Tquio, por seu lado, encerrou nos 14867,26 pontos, mais 206,77 pontos, ao contrrio de Hong Kong, que terminou em baixa ligeira. +Wall Street, a meio da sesso encontrava-se em alta, com o ndice Dow Jones a cotar-se nos 4493,85 pontos, mais 2,77 pontos da vspera e muito prximo da barreira psicolgica dos 4.500 pontos. +No seu todo o dia foi muito positivo, afirmou um trader, que salientou estarem os mercados bastante activos. +Analistas disseram que a reunio do G7 pode terminar com o diferendo entre Washington e Tquio relativo ao sector automvel. +Dos vrios tipos de fundos existentes, os que registaram maior procura, proporcionalmente sua quota de mercado, foram os internacionais. +Os pouco mais de 3,3 milhes de contos que geriam no final do ano passado passaram no final de Junho a 21 milhes de contos, ou seja, um crescimento de 536 por cento. +A este aumento no alheio o facto destes produtos serem os que actualmente oferecem as maiores taxas de rentabilidade, a par dos fundos de aces. +A especilaizao do mercado foi, segundo lvaro Peixoto, a nota mais importante do semestre. +Para o secretrio geral da ASGFIM torna-se, no entanto, necessrio proceder a certas alteraes dentro do sector, por forma a torn-lo mais competitivo. +Uma das reivindicaes a urgente alterao da actual lei que baliza o comportamento dos fundos. +A lei de 1988 e obriga a que as sociedades gestoras tenham pelos menos 25 por cento das suas aplicaes em dvida pblica nacional e 75 por cento das aplicaes tm que ser em ttulos cotados em Bolsa. +A vitria, por 3-0, do Estrela da Amadora frente ao Desportivo de Chaves indiscutvel, como tambm indesmentvel que aquilo que se viu ontem na Reboleira pouco teve a ver com futebol. +Os visitados, que fizeram uma exibio sofrvel, s depois de os transmontanos ficarem com menos um jogador conseguiram criar jogadas com princpio, meio e fim. +O jogo dos visitantes descreve-se com uma palavra: pauprrimo. +Tudo possvel encontrar no IX Salo de Antiguidades e Coleccionismo, desde objectos de ouro e prata, moedas, moblias clssicas, armas, tapearias, pinturas, livros e postais antigos, onde marcam presena de destaque as peas de Art Deco, a mais nova das antiguidades. +Estes trs items foram considerados pelos alunos -- e at por professores do ensino secundrio -- como os mais polmicos da primeira chamada. +No que diz respeito analogia fluido est para vtreo assim como viscoso est para translcido, o jri considerou que a validade do contedo da questo muito reduzida, dado que o grau de dificuldade da relao estabelecida horizontalmente, acrescida do facto de os termos terem um significado em Fsica no coincidente com o da linguagem corrente. +Assim, todos os candidatos recebem os dois pontos atribudos a este item. +O investimento directo portugus no estrangeiro elevou-se no perodo em anlise a 482 milhes de dlares, o que mais 48,31 por cento do que o verificado no perodo homlogo do ano de 1991, e que se explica em grande parte pelo aumento do investimento industrial em Espanha. +Quase metade dos valores registados destinam-se ao pas vizinho e, desse montante, cerca de 95 por cento ter sido aplicado na indstria. +O investimento lquido nacional em ttulos estrangeiros, por seu turno, elevou-se a 298 milhes de dlares quando no ano anterior praticamente no tinha expresso. +Um incndio destruiu, ontem tarde, quatro habitaes de madeira do bairro de pescadores avieiros de Vila Franca de Xira, desalojando um total de dez moradores, que no sofreram quaisquer danos pessoais. +Traves -- Discos frente, tambores atrs. +Rodas -- Pneus 185/60 R 14. +Entre 1 e 4 de Setembro decorreu em Dsseldorf, a edio Primavera-Vero 1992 da Igedo, certame de vesturio, no qual Portugal participou como pas-parceiro. +A Igedo comeou em 1949 com apenas sete exibidores e hoje cresceu at aos dois milhares e meio, em representao de 47 pases. +; e a sua relevncia tanto maior quanto o seu calendrio a torna um barmetro da estao que antecipa. +Os pases mais representados em termos de exibidores so a Frana, a Itlia e, obviamente a Alemanha, enquanto os maiores compradores vm de Blgica, Escandinvia e Holanda. +Coordenadas que fazem com que a Igedo seja especialmente atractiva para as empresas portuguesas viradas para a exportao, sobretudo quando o processo de adeso CEE torna o momento decisivo para o que um dos sectores produtivos de maior peso na economia do nosso pais. +Uma senhora, porm, que no resistiu a abraar com algum orgulho o vereador Oliveira Dias, chamando-lhe ex-camarada (recorde-se que Oliveira Dias foi, durante largos anos, o rosto da CDU na autarquia portuense), abeirou-se de Gomes para lhe dizer que os armrios que a Cmara instalou na avenida so exguos para guardar a mercadoria. +O presidente desculpou-se, lembrando-lhe, mais uma vez, que tudo aquilo provisrio. +Quem no colocou nenhum obstculo foram as vendedoras de peixe, que saudaram efusivamente o autarca.culo foram as vendedoras de peixe, que saudaram efusivamente o autarca. + filho, d-me um abrao, que eu fao sempre campanha por ti e eu hoje no cheiro a peixe como no outro dia, afirmava, emotiva, uma senhora de meia-idade. +Para o autarca vila-franquense, a hesitao do PSD na discusso desta matria estar relacionada com a posio assumida pelo ministro das Obras Pblicas, Transportes e Comunicaes, em reunio recente com a Cmara e a Assembleia Municipal. +Transpareceu a tentativa de evitar discutir este problema e, depois, uma certa vontade de que o assunto fosse dado como definitivamente arrumado. +Finalmente, o ministro acabou por admitir que esta questo ter que ser analisada futuramente, no deixando de considerar que a abolio ter que se verificar, mas sem definir qualquer horizonte temporal, sustenta o presidente da Assembleia Municipal. +Carlos Arrojado aguarda que, depois das frias parlamentares, o projecto-lei e a petio pela abolio das portagens sejam discutidos, em plenrio da Assembleia da Repblica, at ao final deste ano ou princpio de 93. +Os interesses da Brisa e do Governo, ao nvel do Oramento de Estado, no podem continuar a contrariar a necessidade de criar melhores condies de vida populao, acrescenta o autarca, que salienta que por haver portagens no pode ser decidida a proibio da circulao de pesados no interior de Vila Franca e Alverca. +A nica justificao para no haver a abolio de portagens de que, ali, se recebe muito dinheiro, conclui o presidente da Assembleia Municipal. +O Braga conseguiu ontem em Chaves a sua primeira vitria no campeonato nacional, por 2-1. +Tal como aconteceu na partida frente ao Farense, Wosniak voltou a ser a principal figura do encontro, mas, desta feita, pela positiva. +Bem pior est o Chaves, que continua sem ver a cor dos pontos. +E, ao fim de trs jornadas, a formao de Jos Romo soma outras tantas derrotas. +O Presidente da Assembleia da Repblica solicitou a vrios juristas ligados ao seu gabinete que se pronunciem sobre a legalidade da forma como tem sido descontada no vencimento de deputados do PSD a quantia correspondente s multas aplicadas por faltas de comparncia. +Barbosa de Melo foi sensvel s reclamaes de alguns parlamentares da maioria que a ele se dirigiram pondo em causa tais descontos sem deles terem sido previamente informados e, segundo o PBLICO apurou, admitiu mesmo vir a solicitar um parecer Procuradoria Geral da Repblica. +Na reunio de ontem, o lder dos sociais-democratas portuenses no deixava qualquer indcio sobre a forma como ir conduzir todo o processo face s candidaturas que se desenham. +Lembrava apenas que tudo est ainda em aberto e, por isso, insistiu na necessidade de no haver declaraes precipitadas de apoio at porque poderiam fragilizar eventuais candidaturas que possam vir a aparecer. +E contrariando as teses de que o prximo presidente do PSD ser fatalmente um lder de transio, defendeu a aposta numa soluo para quatro anos. +se o PSD interiorizasse a ideia de que seria uma liderana a prazo estaria a admitir desde j que os resultados das prximas eleies autrquicas iriam redundar num novo desaire eleitoral. +Pelo contrrio, Menezes considera que os resultados obtidos nas eleies presidenciais so potenciadores de uma vitria nas autrquicas. +Sem iludir a derrota, defendeu que em causa esteve ainda o julgamento do cavaquismo conjugado com um estado de graa do governo socialista, circunstncias que lhe permitem concluir que a margem de 46% dos votos obtida por Cavaco Silva , apesar de tudo, animadora. +Para alm do mais, diz Menezes, o PP saiu destas presidenciais fragilizado, e comeam a ser notrias as fracturas internas. +o PSD tem de se afirmar como uma oposio credvel, com uma liderana forte, at porque nas previses de Menezes tambm entram as medidas impopulares do governo socialista que estaro a a chegar ... +Ora a est ... +talvez chegue mesmo auto-estrada. +Perigosamente. +O citado desvio, pela escarpa do Observatrio, aproxima-se dos acessos Ponte do Freixo e auto-estrada. +Nada mais nada menos que o Itinerrio Principal n.1, o eixo virio mais importante e carregado do nosso pas. +Logo que a V.C.I. do Porto fr dada por concluida, um autntico formigueiro de carros em andamento. +Sempre na nsia de, primeira oportunidade, cortarem pelo caminho mais recto. +As autoridades paquistanesas criticam a ndia pelos seus planos de desenvolver msseis balsticos, sobretudo o Agni, que tem um alcance de 2500 quilmetros, e o Prithvi, de mdio alcance. +O ano passado, o Paquisto enviou uma carta ao secretrio-geral da ONU depois de o Washington Post, citando os servios secretos americanos, ter noticiado que a ndia instalara msseis Prithvis prximo da cidade de Jullundur, no estado do Punjab, no noroeste. +Nova Deli desmentiu a notcia. + ndia e ao Paquisto reconhecida a capacidade de fabricar -- e de j possuir -- armamento nuclear. +Ambos se recusaram a assinar, no final de 1996, o tratado de interdio total de testes nucleares (CTBT, segundo o acrnimo ingls). +Helena Vaz da Silva, uma ex-jornalista, manifestou a opinio de que a qualidade vende e que os ' media ' ganhadores so aqueles que conseguem conjugar qualidade, lucro e interveno cvica. +Menos optimista sobre a relao qualidade / venda est o analista econmico Francisco Sarsfield Cabral, que apontou o caso do mercado britnico, onde os tablides vendem dez vezes mais do que os jornais de qualidade. +O tambm ex-jornalista da RTP lembrou que se est a voltar ideia de que o accionista deve interferir na vida das empresas, mas referiu que o jornal para ser credvel, no pode ser portador de recados do seu proprietrio. +Para defesa da credibilidade do prprio jornal e para permitir aos leitores um julgamento, disse, a estrutura dos accionistas deve ser conhecida como, alis, acontece na maior parte dos casos em Portugal. +Sarsfield Cabral recordou que os ganhos dos investidores mediticos devem ser financeiros como acontece noutras reas empresariais, mas no devem ser esquecidos os outros tipos de proventos que eles recolhem: A influncia poltica e a influncia social. +Freneticamente, o Governo tem-se empenhado em gastar centenas de milhares de contos em propaganda para nos convencer das vantagens do aumento em 50 por cento da capacidade (seis faixas) da ponte em termos rodovirios e noutra inquestionvel vantagem que a futura implantao nela do caminho-de-ferro. +sero 120.000 passageiros luxuosamente transportados todos os dias e que, como bvio, deixaro de circular de automvel. +Depois, exagera desmedidamente com os paternais cuidados com os utentes da Ponte 25 de Abril, ao ponto de, aps tantos benefcios, ainda lhes querer dar mais uma ponte. +(...) Depois de tantas melhorias, qual a necessidade da nova ponte? +No se estar a comportar o Governo como aqueles babados paizinhos que estragam os filhos com guloseimas? +Estaro os esforados ministros a prever uma exploso demogrfica no pas? +Os responsveis pelas condies de trabalho da imprensa, do Ministrio dos Negcios Estrangeiros, j montaram, entretanto, no CCB, 630 telefones directos, 25 faxes, cinco aparelhos de telex e 350 mquinas de escrever para os, certamente poucos, jornalistas que no usem o respectivo computador. + primeira vista, se compararmos os nmeros -- de jornalistas e de telefones, por exemplo --, no parece estar garantido de antemo que, nas horas de ponta, se evitem os engarrafamentos. +Confrontado com o pedido da mudana toponmica, o municpio viseense, atravs do seu lder, Fernando Ruas, oficiou a Junta no sentido de dar o nome de Lus Martins a uma outra avenida que atravessa a freguesia e que se situa num troo da Entrada Nacional 2 gerido pela Cmara -- o que no agradou aos elementos da autarquia repesense. +Jos Ferro, presidente da Junta, referiu que, mesmo assim, a proposta da edilidade vai ser analisada numa prxima sesso da Assembleia de Freguesia, a realizar em Abril. +Um americano de 43 anos, John Esposito, que se entretinha a sequestrar crianas numa cela metlica enterrada no jardim da sua casa em Nova Iorque, que vigiava atravs de circuitos internos de vdeo, entrega-se polcia. +O custo de vida no Funchal superior ao de Lisboa, revelam estudos que apontam as taxas porturias praticadas no arquiplago como causa do desequilbrio observado. +O aparecimento de filtros e de certo tipo de tabaco mais suave em cigarros, destinados a evitar os cancros das vias respiratrias, esto a multiplicar os casos de uma certa forma de cancro do pulmo, adianta um estudo realizado pela Sociedade Americana de Cancro (ACS). +At aos anos 50, o fumo do tabaco usado nos cigarros era extremamente irritante e, em consequncia disso, era difcil inal-lo profundamente. +A maior parte dos fumadores realizava, assim, apenas uma inalao parcial e a maior parte do fumo ficava-se pelas vias respiratrias superiores e depositava-se na garganta e na boca. +Era a que se fixava a maior parte das substncias cancergenas e a maior parte dos cancros dos fumadores surgiam nestas regies. +Foi quando apareceram os filtros e os tabacos mais suaves que os fumadores comearam a inspirar o fumo mais profundamente, levando-o mesmo at aos alvolos pulmonares, permitindo a criao de depsitos de substncias cancergenas nas vias mais finas, na periferia dos pulmes -- o que deu origem ao desenvolvimento de uma forma de cancro chamada adenocarcinoma. +Pinochet passeou-se por Portugal, tudo numa visita oficialmente privada. +Governo e Exrcito fizeram vista grossa passagem do general. +Eis quando seno um jipe dos Comandos deu o no dito por dito. +Certo que Pinochet fez do pas uma casa portuguesa, com certeza. +Secreta foi uma reunio nos arredores de Lisboa e uma saltada-relmpago a Londres. +O chefe do grupo conservador Rossia no Parlamento russo, Serguei Babourine, concordou que no existe nenhuma base legal para julgar as aces de Erich Honecker enquanto chefe de Estado de um pas internacionalmente reconhecido. +O prprio Gorbatchov considerara imoral a expulso de Honecker mas a posio poltica em que se encontra actualmente o Presidente sovitico no a mais propcia para fazer fazer as suas opinies. +No dia dedicado a debater as questes do emprego, o antigo presidente da Comisso Europeia brindou-os com as suas quatro condies para o xito da moeda nica (uma questo que divide profundamente os alemes), entre as quais esto um pacto de confiana entre os pases que ambicionam participar na terceira fase da UEM e um novo sistema monetrio europeu que crie o quadro das relaes entre os que aderem numa primeira vaga e os que ficam de fora. +Falando ao corao dos sociais-democratas, numa crtica velada ao ministro das Finanas Theo Waigel, Delors afirmou que a poltica econmica no s uma questo de oramento e de moeda a cheirar a thatcherismo defunto, mas deve ter tambm em conta a dimenso humana, que envolve os salrios e o emprego. +Mas no deixou de advertir para o facto de o pleno emprego no ser possvel com dfices. +Estas so algumas das novas linhas directivas do TPC anunciadas recentemente pelo Ministro da Educao britnico, David Blunkett, para as escolas pblicas. +Estudos recentes revelaram que metade dos alunos da quarta classe no estavam a receber TPC compulsivo. +A GNR de Barcelos deteve esta semana um indivduo de 28 anos, solteiro, desempregado, residente na freguesia de Carvalhas, acusado de ter agredido e tentado violar uma viva de 48 anos, residente na mesma localidade. +O alegado violador foi detido em sua casa, depois de a vtima ter denunciado o caso s autoridades policiais quando se encontrava no Hospital Distrital de Barcelos para receber tratamento. +Os factos, segundo a queixa apresentada, ocorreram ao incio da noite, num caminho ermo, sem qualquer casa prxima, uma passagem pouco utilizada na aldeia. +No momento em que regressava a casa, o indivduo lanou-se sobre ela, tapou-lhe a boca, agrediu-a e atirou-a ao cho. +Depois rasgou-lhe a roupa e tentou consumar a violao. +A mulher ofereceu resistncia e, alguns minutos mais tarde, apareceu um filho da vtima, de 16 anos, que pegou num pau e agrediu o alegado violador. +Na Escola Superior de Educao Jean Piaget, de Macedo de Cavaleiros, est instalado o clima de medo. +Nesta vila transmontana tm ocorrido algumas situaes de confronto entre estudantes e grupos de jovens locais, a ltima das quais teve lugar na madrugada de sbado passado e resultou em ferimentos com alguma gravidade num aluno. +Para resolver o problema, o director da instituio de ensino universitrio vai hoje reunir com o presidente da Cmara e o comandante da GNR locais. +Centros urbanos e candidatos independentes so os denominadores comuns dos pequenos partidos na disputa autrquica. +Tudo por causa dos poucos meios financeiros e das restries da lei candidatura de listas de cidados. +Mais uma semana que passou mais, mais uns mximos histricos estabelecidos pela Bolsa de Nova Iorque. +Apesar de a economia norte-americana estar a dar mostras de um arrefecimento, as aces cotadas em Wall Street continuam a valorizar-se de uma forma progressiva e continuada. +Na semana que passou os fundos de investimento foram os investidores mais activos. +O ndice Dow Jones fechou nos 4585,84 pontos, mais 1,66 por cento face semana anterior. +O director responsabilizou ainda a Redaco pela degradao da situao de O Primeiro de Janeiro, exigindo maior empenho por parte dos jornalistas. +, porm, perceptvel que s aparentemente os dois mundos tm vivido completamente separados. +Por um lado -- salientando os aspectos positivos --, as produes estrangeiras conduziram a um apetrechamento tcnico dos prestadores de servios nacionais, constituio de excelentes equipas, rodagem de actores que beneficiaram as produes nacionais e, nalguns casos, promoveram internacionalmente a nossa imagem. +No entanto, ao mesmo tempo, elas muitas vezes vampirizam a imagem do pas, determinam um aumento nos custos de produo em Portugal -- vide, de novo, as declaraes de Cunha Telles --, o que, por sua vez, conduziu a que o IPC deixasse de ter possibilidade de suportar, para o mesmo nmero anual de produes, a totalidade do seu custo e marginalizaram os nossos criadores -- realizadores, guionistas e msicos -- que s muito excepcionalmente foram chamados a intervir nessas produes. +A necessidade de obter financiamentos fora dos apoios do IPC tem levado a uma alterao estrutural das relaes produtores / realizadores no mbito do cinema portugus, com o reforo do papel dos primeiros, que deixaram de ser meros gestores dos subsdios obtidos pelos segundos para passarem a ser portadores do valor acrescentado que o financiamento, normalmente externo, que complementa os apoios locais -- IPC e RTP -- e permite a montagem financeira do projecto. +Por sua vez, o reforo do papel dos produtores e a presena de produtores estrangeiros trazem alteraes decisivas na prpria natureza dos projectos a montar, passando naturalmente a existir uma ateno diferente em relao ao espectador e, consequentemente, s perspectivas comerciais da obra. +Do alto de uma varanda, microfone na mo, ele atordoa a praa, vibra e faz vibrar todos os que o escutam. +S Sampaio parece no ser muito admirador do estilo, cultivando uma sobriedade pouco compatvel com o espectculo que lhe foi preparado. +Mas a verdade que foi em Abrantes que o lder socialista teve a melhor entrada e apresentao da campanha. +Quando chega ao palco a assistncia j est rendida. +Resta apenas dar substrato poltico a uma adeso emocional garantida. +O responsvel por este ambiente um jovem de 28 anos, lder da JS de Abrantes e apontador de profisso. +Trabalha na Cmara local, mas a sua paixo mesmo o espectculo, a rdio, o jornalismo. +Isto comeou na escola. +Logo naqueles espectculos de escola, mostrei que tinha um certo jeito para aquilo, explica Manuel Maurcio. +Mais tarde inscreveu-se numa escola de msica e no perdia tudo quanto era festinha. +Solicitado para apresentar, ler poemas, animar a malta. +Um entertainer ribatejano que foi ganhando notoriedade. +Nestes ltimos dias, os bracarenses apenas tm estudado pormenores do jogo, pois, quando duas equipas de nvel semelhante se encontram, os pormenores que decidem, reforou Donner, que vai iniciar o jogo com uma defesa 3x2x1, agressiva quanto baste. +Depois, na transio para o ataque, Mrio Costa e Paulo Faria (ainda em dvida devido a uma leso) saem para dar lugar aos atacantes Bolotskih e Dobrescu. +tem o ttulo praticamente assegurado. +Jos Saldanha mostra-se ainda muito surpreendido com a suspenso de que foi alvo a ETEM por parte da Direco-Geral de Armamento (DGA) e acusa este organismo de no assumir as suas responsabilidades. +Alegando ter tomado conhecimento da suspenso atravs do PBLICO, Jos Saldanha diz que as responsabilidades tm que ser assumidas pela DGA que recebeu todos os documentos da operao e autorizou a exportao. +quem assinou os documentos de exportao no fui eu. +Jos Saldanha acusa a DGA de ter um comportamento ambguo e vago nesta histria, dando cobertura a investigaes sem credibilidade nenhuma que foram utilizadas na Bolvia num contexto de luta poltica pelo poder nas ltimas eleies presidenciais. +Saldanha no quer ser o bode expiatrio deste caso e admite meter um processo judicial contra o Estado portugus, com pedido de indemnizao. +A situao dos professores de Portugus a leccionar no estrangeiro dominou a reunio, na passada quinta feira, da Federao Nacional dos Sindicatos da Educao (FNE) com as secretarias de Estado da Administrao Escolar e da Inovao e Educao. +Uma das vitrias reivindicadas pela FNE a integrao destes professores no Estatuto da Carreira Docente dos educadores de infncia e dos professores do ensino bsico e secundrio. +Uma medida que, no entender da entidade sindical, s peca por tardia e vai alterar o decreto-lei de 1979 que regulamentava a actividades desses professores, estabelecendo-se agora novas condies e regras para o exerccio do ensino da lngua portuguesa no estrangeiro. +A exigncia de clareza nos concursos para o recrutamento dos professores a leccionar no estrangeiro foi outra das reclamaes da FNE, que rejeita a prtica actual, sujeita a despachos pontuais. +Defende, por isso, que os concursos para recrutamento de professores passem a ser regulados por decreto. +Outra exigncias da estrutura sindical foram que o portugus como segunda lngua faa parte integrante dos currculos das comunidades emigrantes no estrangeiro, que o sistema de segurana social dos professores assegure a proteco na sade e que o vencimento-base seja acrescido de um subsdio de custo de vida. +A guerra das pedras deu credibilidade internacional luta dos palestinianos e obrigou Arafat a renunciar ao terrorismo e a reconhecer Israel. +Mas a frustrao pela degradao das suas condies de vida e, sobretudo, a m orientao da liderana palestiniana, fizeram com que os palestinianos perdessem quase tudo o que tinham conquistado, quando decidiram apoiar Saddam Hussein na crise do Golfo. +Os Cocteau Twins fecharam a 18 edio do Printemps de Bourges, infelizmente sem aquilo a que se pode chamar uma chave de ouro. +Valeu uma compensao chamada Morphine, desconhecida de muitos, mas que s veio confirmar a excelncia do programa deste ano do festival francs. +Os comentrios de Miguel Sousa Tavares (MST) inseridos no artigo do PBLICO de 4 de setembro sob o ttulo em epgrafe so deveras esclarecedores no que concerne aos tributos dos cidados. +tem que ver com a contribuio autrquica no que respeita a bens no geradores de rendimento, como o caso de terrenos classificados de urbanos, a partir de 1989. +Depois, verifica-se uma enorme discrepncia de valores na sua avaliao para efeitos fiscais, sempre gravosa, sem atentar sequer nas flutuaes do mercado e das prprias leis. +mas, se o objectivo foi libertar os terrenos, evitando a estagnao, seria necessrio que as autarquias respondessem s solicitaes, o que no acontece. +Assim, encorajado pela total disponibilidade do meu interlocutor, que igualmente admito, tentaria que entendesse, pelo menos, as vantagens econmicas, em termos de desenvolvimento local (turstico, por exemplo), da salvaguarda da invulgarmente importante, mesmo escala mundial, jazida com pegadas de dinossurios da serra d'Aire, cujo valor cientfico j trouxe aqui cientistas de todo o mundo (dos EUA China), e da transformao do stio num local pblico com todos os equipamentos necessrios sua fruio como bem cultural, cientfico e pedaggico. +Quanto a este assunto, tentaria convencer o professor a visitar o local e procurar saber as opinies dos responsveis locais e das respectivas populaes e, ainda, a auscultar o sentir dos vrios sectores da vida portuguesa sobre este caso, incluindo o do cidado annimo. +A propsito da exposio Dinossurios da China, em curso no Museu Nacional de Histria Natural, comearia por insistir no convite a Sua Excelncia a visit-la. +Aproveitaria para lhe mostrar o estado lamentvel de um enorme casaro, esventrado e em tosco, na sequncia do incndio da Faculdade de Cincias (h 18 anos!), de reinstalao sempre adiada e pomposamente referido como Museu Nacional, que pouco deve tutela, mas ao qual se reconhece uma obra cientfica, cultural e pedaggica notvel. +Dir-lhe-ia que reunir aqui esta magnfica coleco de verdadeiros fsseis, alguns gigantescos, dos terrenos mesozicos da velha China foi mrito exclusivo deste Museu, que no contou com quaisquer apoios diplomticos, numa realizao que vai fazer mais pelo estreitamento das relaes entre os dois pases do que quaisquer outras j realizadas. +As centenas de milhares de visitantes esperados assim o permitem concluir. + Dir-lhe-ia, ainda, como lamentvel neste caso a falta de apoio do Ministrio da Educao. +De resto, os comerciantes espanhis parecem estar mais atentos ao que se passa no Algarve do que os colegas portugueses. +Alm do po, tambm a pastelaria espanhola passou a ocupar as prateleiras dos supermercados, a preos concorrenciais. +Quanto s discotecas, parecem ser o mealheiro dos jovens -- nelas que investem a principal fatia do oramento de frias --, apesar de tambm neste ramo os espanhis estarem a descobrir um filo. +Fiis aos seus pubs em Albufeira, continuam os ingleses, mas longe vo os tempos em que com um libra -- em 1988 valia 285 escudos, hoje fica-se pelos 230 -- faziam figura de reis e senhores. +A primeira das crticas ontem apresentadas foi para o novo sistema retributivo, que os enfermeiros consideram nunca se ter adaptado carreira de enfermagem. +Segundo explicou o dirigente Jos Azevedo, a entrada em vigor dos novos diplomas trouxe consigo inmeras anomalias, geradoras de injustias gritantes, como o caso dos profissionais que, depois de subirem na carreira, ficam a ganhar menos do que antes da progresso. +Quanto a horas extraordinrias, os enfermeiros salientam no terem acesso s vultuosas verbas que nelas se consomem e que para alguns profissionais da sade funcionam mais como complementos disfarados de vencimento do que como resposta a necessidades efectivas. +Outros processos a contribuir para o descontentamento da FENSE so os que dizem respeito ao descongelamento de vagas, criao de uma Ordem dos Enfermeiros e s equivalncias de ttulos acadmicos. +Em relao ao primeiro, a desactualizao dos quadros das instituies cria um desajustamento em relao s necessidades que faz com que, na prtica -- diz Jos Azevedo -- nem um tero das vagas disponibilizadas seja aproveitado para colocar novos enfermeiros. +Resultado: h, ao mesmo tempo, instituies com falta de pessoal e enfermeiros no desemprego. +O que a FENSE pede um regime de excepo na contratao de novos enfermeiros, como o exigem as circunstncias. +Professor. +Por que razes escolheu fazer frias em Portugal? +Foi uma escolha acertada? +R. -- uma constatao, uma diferena de estilo. +Sempre fui muito claro nas minhas convices. +Acho que a militncia partidria toda a gente percebeu que no vou ter, nem terei, nem tenho tido. +Mas no faz sentido, em meu entender, no quadro actual da democracia portuguesa, fazer isso hoje. +Acho que a avaliao que as pessoas tm que fazer sobre as personalidades, sobre o perfil e a sua adequao funo tem muito a ver com as pessoas e no com ter ou no ter um carto. +P. -- Mesmo que seja eleito, no se desvincular do partido? +Muito provavelmente, a resposta oficial ser positiva. +Apesar de parecer paradoxal, Milosevic e o seu partido tm agora boas razes para alguma esperana, pelo menos para a sua prpria sobrevivncia poltica no poder, se no mesmo para um futuro que poder revelar-se brilhante e colorido. +Os antigos (e agora reformados) comunistas conseguiram regressar ao poder em diversos pases (Hungria, Polnia, Bulgria) e uma opo poltica similar tem tambm agora boas possibilidades nas prximas eleies presidenciais na Rssia, marcadas para Junho. +Por saber que desempenha um papel decisivo na sua nova funo de construtor da paz na Bsnia -- sob os desgnios da Pax Americana delineada em Dayton, Ohio -- Milosevic espera que ele prprio e o seu regime na Srvia possam estar a salvo. +Entre as promessas, a mais importante foi a de que tenciona retirar as tropas russas da Tchetchnia, embora no tenha dito quando, mas apenas que no ser na totalidade, e no seja a primeira vez que anncios semelhantes so feitos nas vsperas de novas ofensivas militares. +Queremos retirar as tropas at s fronteiras da Tchetchnia, disse Ieltsin durante uma visita a Tcheliabinsk, nos Urais. +Se retiramos totalmente, os bandidos degolaro imediatamente toda a populao civil. +Eles no se ficaro por a. +Iro at ao Daguesto, ao Karachaevo-Tcherkssia e a outras repblicas do Cucaso do Norte, para as ocupar, e haver terrorismo e banditismo internacional. +Programa Quadro de Cincia e Tecnologia da Unio Europeia. +Neste captulo, a principal vantagem da nossa adeso foi a abertura de novos horizontes, o lanamento de relaes com grupos de investigao e empresas -- a gerao de um fluxo de informao que permitiu abrir novas perspectivas no sistema de investigao cientifico e tecnolgico nacional. +Jos Antnio relembra que Max era levado da breca para contar histrias e para pregar partidas. +Um dia, o Humberto Madeira, o Raul Solnado e o meu pai iam em viagem e passaram pelo pinhal de Leiria. +O Humberto Madeira resolveu contar que esta era uma zona onde, em tempos, tinha havido muitos assaltos. +No regresso, o meu pai j vinha a dormir. +Rebentou um pneu e ouviu-se um grande estrondo. +A sua distraco ia ao ponto de telefonar para casa a perguntar se o filho se chamava Jos Antnio ou Antnio Jos. +Embora no soubesse uma nota de msica, trauteava e compunha por intuio. +Tinha um espectacular ouvido musical e era capaz de distinguir os sons de cada instrumento na mais complicada melodia. +Sou assduo na melhor esplanada da cidade, no Molhe. +H vinte anos, era arriscado l ir, por causa dos marginais. +Depois, tornou-se local de convvio da mais alta qualidade. +Mas receio que esteja outra vez a entrar num mau perodo, com a explorao excessiva e de mau gosto. + No prximo ano, devido ao princpio da gratuitidade dos transportes escolares preconizado na lei de bases do sistema educativo, os alunos do oitavo ano tero tambm transporte gratuito, o que agravar em 17 por cento o custo comparticipado pela Cmara. +Aos alunos de Alqueiro, Casal das Pimenteiras, Casal da Fonte e Beselga a autarquia assegura a utilizao de carreiras pblicas ou at de txi por considerar que o percurso a p oferece riscos s crianas. +De Wilde, Costinha, Lus Miguel, Gil Baiano, Marco Aurlio, Beto, Pedrosa, Oceano, Vidigal, Pedro Martins, Peixe, Pedro Barbosa, Afonso Martins, Dominguez, S Pinto, Yordanov, Ouattara e Paulo Alves foram os futebolistas escolhidos por Octvio. +Ficaram de fora, alm dos chamados s seleces, Vujacic, Tiago e Balajic. +A cena tirada de uma pgina asfixiante de Joseph Conrad: um cadver em pijama, deitado numa cama com uma manta verde nos ps, dentro de uma pequena cabana de madeira a tresandar a luto, flores e formol, velado por a viva, por a filha e com uma guarda de honra de cinco jovens soldados Khmer. +No restam dvidas que o corpo dele e que est morto, garantiu um dos jornalistas nesse grupo, o americano Nate Thayer, correspondente da Far Eastern Economic Review e o estrangeiro que melhor conheceu a histria do fundador dos Khmer Vermelhos. +Pol Pot morreu oficialmente com um enfarte cardaco, quarta-feira noite, mas inevitvel pensar em causas menos naturais para o seu desaparecimento. +As reaces foram cautelosas e os EUA, porque nos maus filmes de terror os mortos-vivos conseguem sempre levantar-se, exigiram uma autpsia. +Na passada Primavera, a Assembleia de Freguesia do Carregado solicitou Cmara de Alenquer, de maioria socialista, o processo de candidatura da elevao a vila, mas j em Setembro, Vasco Miguel, do PSD, entregava no Parlamento um projecto de lei nesse sentido, atitude que o presidente da Cmara classificou de oportunismo poltico, alegando que o diploma caducaria com o final da legislatura e no chegaria a ser discutido, o que de facto aconteceu. +Agora, o PSD voltou a pegar no assunto, pelas mos de Duarte Pacheco, que sustenta a retomada desta iniciativa na vontade das pessoas que nasceram e vivem no Carregado e no concelho de Alenquer. +Duarte Pacheco alega que o Carregado a zona do concelho de Alenquer com maior ndice de crescimento industrial e tambm um dos maiores centros populacionais da regio, pelo facto de se situar num dos maiores ns rodovirios do pas, acrescido das ligaes fluviais e ferrovirias. +O ministro portugus da Administrao Interna avistou-se ontem com o ministro holands da Justia para receber os dossiers sobre a imigrao e do Grupo de Trevi que transitam da presidncia holandesa da Comunidade para a portuguesa. +A questo da criao da Europol (uma polcia comunitria) foi um dos temas em anlise no encontro. +A passagem do testemunho, entre Haia e Lisboa, em matria de poltica interna comunitria apenas se fez agora por indisponibilidade da agenda do ministro holands da Justia. +O Holanda-Inglaterra decide quase tudo neste grupo. +Quem ganhar fica praticamente com o visto no passaporte para os EUA, enquanto Noruega basta um ponto na Polnia para pela primeira vez participar numa fase final do Mundial. + claro que as contas podem todas complicar-se, caso holandeses e ingleses empatem e polacos venam noruegueses. +Nesse caso, a Polnia permaneceria na corrida. +A deciso do Grupo ficaria ento muito complicada, com trs equipas com hipteses de chegar ao fim com 14 pontos e a ser preciso recorrer ao foto-finish para saber quem se apuraria. + o maior grupo, com sete equipas. +Para j Repblica da Irlanda e Dinamarca ocupam os dois primeiros lugares, mas a Espanha ainda est na corrida. +O Repblica da Irlanda- Espanha de hoje j ajudar a definir posies, mas dever ser o Espanha- Dinamarca de 17 de Novembro que tudo esclarecer. +A no ser que o Eire vena hoje a Espanha e a Dinamarca derrote a Irlanda do Norte, ficando Eire e Dinamarca de imediato com a presena nos EUA garantida. +O objectivo genrico destas iniciativas, patrocinadas pela Unio Europeia, o desenvolvimento de uma poltica global de reduo da insegurana, volta de quatro eixos fundamentais: respeito pelos direitos humanos, participao activa dos cidados na definio e aplicao da poltica de segurana requerida, aposta em solues de partenariado envolvendo os sectores pblico e privado e preocupao com a segurana do cidado, para evitar que ele se torne autor ou vtima de crimes. +As actividades amadoras vo ser desinflaccionadas negociando o ordenado dos atletas. +No o marketing e a sponsorizao que resolvem os problemas. +Esta nova equipa prope-se fazer um trabalho honesto e claro. +O Benfica j conta, neste momento, com setenta mil scios e sete mil praticantes. +Os scios vo poder criar lugares para os seus filhos praticarem desporto. +Manuel Monteiro mentiu aos portugueses. +O que j escrevemos esta semana excitou as conscincias do Partido Popular, ciosas em mostrar que o processo da lista de candidatos por Lisboa correu na maior das lisuras, incluindo o folhetim Lus Nobre Guedes. +no dia 9 de Junho o lder dos populares disse que j tinha a resposta de Nobre Guedes ao convite para encabear a lista da capital e torn-la-ia pblica na semana seguinte, depois de a comunicar estrutura distrital. +At ontem, ningum conhecia, pela boca de Monteiro, a resposta ao convite. +So estes os factos e de nada vale que Nobre Guedes venha desagravar o comportamento do seu lder -- a crer na ltima edio do Independente. +Diz o jornal, referindo-se comisso poltica de ontem, que Nobre Guedes sublinhar que o presidente do PP nunca mentiu no decurso do processo de constituio das listas. + assim a transparncia do Partido Popular, feita de recados. +Teresa Vasconcelos -- Digo isto no livro e digo com convico: h muitas Anas neste pas. +Mas tambm h muitas outras que poderiam ser como a Ana e se desmobilizaram. +P. -- Por falta de condies, falta de formao, falta de acompanhamento? +P. -- Foi um colega seu ... +R. -- Faam o favor de tomar nota do que vou dizer, e agradecia que fosse transmitido, porque de vez em quando as coisas no saem de acordo com a verdade dos factos. +A EDP tem tido um comportamento impecvel em todo este processo. +Quando comearam os estudos da barragem de Foz Ca, fez um estudo de impacto ambiental. +Quando se descobriram os vestgios arqueolgicos, fez um protocolo com o Ippar [ Instituto Portugus do Patrimnio Arquitectnico e Arqueolgico ], porque no percebia da matria. +Depois foi a EDP que vedou e policiou toda a rea onde foram descobertos os achados arqueolgicos. +Por isso, se aquilo est recolhido, deve-se EDP. +Quando os protestos eclodiram, os media internacionais divulgaram para todo o mundo informaes sobre uma carnificina em larga escala da populao revoltada da cidade pelas foras fiis a Ceausescu. +Estas notcias alarmistas -- num relatrio recente, os servios secretos romenos acusaram espies hngaros e de outras nacionalidades de terem incitado revolta -- contriburam decisivamente para o derrube do regime, mas mancharam a revolta. +Afinal, os cadveres nus e alinhados descobertos no eram vtimas da represso, mas antes de pessoas pobres, doentes e invlidas, exumadas de fossas comuns e colocadas no local. +Hoje, o monumento aos heris desconhecidos de 1989 no faz qualquer aluso a este episdio e no enorme talho continua a ser enterrado quem no pode pagar uma sepultura decente. +Estvamos em estado de choque, vtimas de uma alucinao colectiva. +Vi o cadver de uma mulher com o seu filho no ventre. +Eram talvez cinco corpos, mas eu via vinte, contou recentemente, ao enviado do Le Monde, Cornel Balint, presidente de uma das seis associaes de revolucionrios de Timisoara, que agrupam cerca de 800 pessoas recompensadas por terem sido feridas, presas ou molestadas durante os combates. +Um dos objectivos dos Encontros -- tal como os definiu h seis anos Madalena Perdigo, fundadora do Acarte -- era permitir o acesso dos coregrafos portugueses aos circuitos europeus, dos quais estavam arredados por desconhecimento mtuo. +De um lado, pela situao geogrfica de periferia e, do outro, pelo desconhecimento das tendncias em vigor nos grandes festivais internacionais. +Os Encontros Acarte propunham-se responder a esse duplo anseio: informar o pblico e estimular os autores portugueses, na perspectiva de os despertar para as estticas do ps e neo-modernismo. +A linha fundamental [ dos Encontros ] servir o pblico, mant-lo dentro da actualidade coreogrfica. +Mas a afirmao da sua identidade nunca se fez pela adopo de uma linha esttica nica. +No h uma direco nica [ no conceito de modernidade ]. +O pblico tem o direito a escolher o que lhe interessa e no creio que seja necessrio conduzi-lo pela mo como se fosse um indigente. +Smidovitch, com um fomos aonde planemos ir, declinou confirmar, no entanto, se a fiscalizao abrangera sedes de ministrios, quinta-feira vedadas categoricamente ao pessoal da ONU pelo regime de Bagdad. +Sobre se haviam encontrado algo suspeito tambm no adiantou pormenores. +A misso de 22 peritos a primeira a chegar capital iraquiana desde que, tambm sem resultados, foi passada revista ao Ministrio da Agricultura, a 28 e 29 do ms passado, misso que ps termo a um impasse de trs semanas entre o Governo de Saddam Hussein e a comisso da ONU. +As inspeces, recorde-se, comearam em Abril do ano passado, pouco depois do fim da Guerra do Golfo e resultaram dos acordos de cessar-fogo ento assinados. +Aps negociaes com o principal patrocinador da equipa, a Shell, Hardwick entregou um dos wild cards (convites) a Felisberto Teixeira, que conta tambm com o apoio da Federao Nacional de Motociclismo (FNM) e est convicto que o portugus far bom trabalho, reconhecendo, porm, que qualquer estreia, e muito especialmente em 500cc, sempre difcil. +No pas prosseguem os incidentes: de percursos para uns, perigosos para outros (Noticirio das 13 horas da Rdio Nacional). +... Os laboratrios do Departamento de Estado e do Pentgono ainda no conseguiram dar uma resposta precisa quanto imprevisibilidade da reaco do seu ex aliado (Jonas Savimbi), que parece ter ingerido demasiadamente esterides anabolizantes nos tempos de ajuda encoberta e ter crescido demais (Correio da Semana, 22.9). + mesmo de supr que se trata de uma das mais importantes exposies fotogrficas dos ltimos anos, a nvel internacional. +O tema da gua, ligado aos oceanos como proposta genrica da Expo-98, permite abordar esse elemento em todos os seus estados, liqudo, fsico e gasoso, e obviamente relaciona-se com o prprio processo fotogrfico. +Sendo, alm de crtico de pera e fotografia, professor de qumica, trabalhando no domnio da termodinmica dos objectos moleculares e das relaes entre artes e cincias, Calado estava habilitado como ningum a comissariar esta exposio. +O Instituto de Investimentos e Privatizaes dos Aores (IIPA) est a negociar um emprstimo de 1,5 milhes de contos, para poder fazer face aos compromissos assumidos na rea dos incentivos de base regional, disse ao PBLICO o presidente do IIPA, Joo Bernardo Rodrigues. +De acordo com aquele gestor, o IIPA contactou o Banco Comercial dos Aores, a sociedade Esprito Santo- sociedade de investimentos e o Banco Pinto & Sotto Mayor. +O emprstimo ser negociado por tranches, j que o valor em causa para o binio 1991-92. +O valor total dos compromissos assumidos de 4,2 milhes de contos, dos quais 1,5 milhes so da responsabilidade do Governo Regional dos Aores via IIPA e o restante das comunidades. +Segundo Joo Bernardo Rodrigues foram aprovados 60 projectos, cujo valor total de investimento da ordem dos nove milhes de contos, distribuidos, entre outros, pela agro-pecuria, construo civil, basaltos, cimentos e para a realiazao de uma nova unidade fabril de cerveja. +Indignados com a ausncia de negros em lugares elegveis, os representantes da comunidade imigrante africana aconselharam o voto em branco. +Fernando K, 41 anos, presidente da Associao dos Guineenses desde 1987, no compreende como num pas com uma presena multissecular africana, no h um poltico, um deputado, um presidente de cmara, sequer um vereador negro. +Deputado suplente do PS com entradas fugazes no Parlamento, acusa Antnio Guterres de trair promessas feitas em Julho a representantes da comunidade e prepara uma campanha internacional contra uma situao que torna os negros portugueses em cidados de segunda classe. +As cidades candidatas organizao dos Jogos Olmpicos de 2004 vo conhecer hoje o regulamento a que devero obedecer, durante a sua campanha de preparao e promoo. +Ser durante um encontro a realizar no Museu Olmpico, em Lausana, na Sua, onde os representantes das 11 cidades se iro reunir pela primeira vez. +Jogo grande foi o que juntou, em Phoenix, os Suns e os Portland Trail Blazers, e que terminou com a vitria dos donos da casa, por 118-109. +A.C. Green, ex-jogador dos Lakers, marcou 31 pontos para liderar os Suns, num jogo em que Sir Charles Barkley averbou apenas 21 pontos. +Discreto no campo, o temperamental Barkley voltou a ser notcia fora dele. +Agora, vai ter de responder em tribunal a uma queixa de agresso interposta por um f. +Barkley acusado de ter agredido Edward Durham num bar, depois de este o ter criticado por empurrar uma senhora que apenas pedia um autgrafo. +Barkley no comenta o incidente, e o mesmo faz a sua equipa. +Os Suns, finalistas da poca passada, esto mais concentrados no campeonato, onde ocupam o segundo lugar da Diviso Pacfico, liderada pelos invictos Seattle Supersonics. +Um mtodo muito comum de movimentar ficheiros na Internet. +O FTP uma forma especial de fazer login noutro local da Internet com o objectivo de carregar ou enviar ficheiros. +H muitos servidores da Internet que contm imenso material que pode ser acedido por qualquer pessoa atravs de um acesso annimo, pelo que so chamados servidores FTP annimos. +Um mtodo bastante utilizado para realizar menus de material disponvel na Internet. +Gopher um programa de utilizao cliente-servidor, pelo que requere que o utilizador disponha de uma verso cliente do Gopher. +Os Gopher difundiram-se muito rapidamente atravs da Internet nos ltimos anos, mas esto a ser suplantados pelo hipertexto suportado atravs da World Wide Web. +Contudo, existem ainda milhares de Servidores Gopher na Internet, que devero subsistir por mais algum tempo. + semelhana de muitas outras igrejas evanglicas, o pagamento do dzimo na Universal no mais do que a adopo de uma prtica bblica. +Ainda hoje o cdigo catlico de Direito Cannico refere que a igreja tem o direito, independentemente do poder civil, de exigir aos fiis o necessrio para o culto divino, para a honesta sustentao dos seus ministros e para satisfazer outros fins prprios ... + o que faz todos os meses a Igreja do Reino de Deus. +Em Portugal, o dinheiro dos crentes colocado num envelope, que os obreiros da IURD, sempre atentos, recolhem e controlam. +No Brasil, existe uma caderneta individual onde cada seguidor regista, religiosamente, as suas contribuies para a igreja de Macedo. +Se a escrita sobre arquitectura no abunda entre ns, Manuel da Graa Dias tem dado um excelente contributo para alterar essa situao. +No s as memrias descritivas dos seus projectos so, s por si, criativos textos sugerindo, com especial eficcia, uma multiplicidade de imagens a partir dos desenhos que apresenta, como a sua produo para a imprensa revela idntica qualidade. +Pelo uso de uma linguagem despojada de excessivas conotaes tcnicas inibidoras da aproximao do leigo, pela originalidade dos sentidos que ajuda a desvendar nos temas do quotidiano.. +Os portugueses esto em forma no surf. +Pelo menos os jovens que esto a participar no Campeonato Europeu de juniores, a decorrer at domingo em Newquay, Inglaterra. +no surf, Joo Macedo, Andr Pedroso (em sub-18), Ruben Gonzlez e Pedro Monteiro (em sub-16) esto j nas meias-finais. +No haver greve nem bloqueio poca da NBA, a Liga Norte-Americana de Basquetebol profissional, independentemente de ainda no existir acordo sobre o tecto salarial, decidiram os proprietrios e jogadores. +Este anncio vem sossegar aqueles que temiam que sucedesse no basquetebol o que j aconteceu no basebol e no hquei no gelo -- a interrupo ou adiamento dos campeonatos. +Sendo assim, e uma vez que o acordo para o contrato colectivo de trabalho, cujas negociaes se arrastam h meses, est previsto para breve, tudo indica que a NBA comear, como planeado, a 4 de Novembro, ou seja, na prxima sexta-feira. +Ainda no foyer, pde-se desde logo perceber que no Servio de Msica da Fundao Calouste Gulbenkian se est em momento de reflexo, j que foi distribudo ao pblico um questionrio onde, entre outros pontos, se pede uma opinio sobre a periodicidade de apresentao da msica contempornea. +Precisamente no concerto dessa noite se propunha um percurso pela msica mais recente intersectado de forma exemplar por sonoridades do passado. +A enquadrar as obras de Wolfgang Rihm La lugubre Gondola / Das Eismeer e de Luigi Nono No hay caminos, hay que caminar .. Andrei Tarkovsky, tivemos a msica de Giovanni Gabrieli (Canzone a tre cori e In Ecclesiis) em orquestraes da autoria de Bruno Maderna. +A relao entre Luigi Nono e Giovanni Gabrieli ultrapassa a simples passagem e vivncia na cidade de Veneza, situando-se principalmente ao nvel da explorao das potencialidades musicais inerentes espacializao sonora. +Apesar de no se ter procedido a qualquer distribuio de msicos no espao do Grande Auditrio Gulbenkian, imagem do que se fazia com as obras do msico renascentista na Catedral de So Marcos, a orquestrao de Maderna reflecte um total respeito pelas estruturas de dilogo entre coros com uma constituio tmbrica diferenciada. +Desde o primeiro dia do ano, entre os palestinianos procurados, 13 foram mortos pelas foras de segurana e 15 feridos mas 40 foram libertados pelo Exrcito, disse Yatom durante uma visita de inspeco regio de Belm (sul da Cisjordnia). +Quatrocentos outros so ainda procurados pelas foras de segurana. +Informaes do Exrcito, em termos globais, do conta de 23 palestinianos mortos e 211 feridos por soldados, desde o dia 1 de Janeiro, na Cisjordnia. +O nmero confirmado pelos palestinianos. +Danny Yatom disse que a fraca proporo de palestinianos mortos em relao ao nmero de palestinianos presos prova que o Exrcito no dispara seno em casos excepcionais, contrariamente a certas alegaes. +R. -- No h uma circulao do pensamento. +H muitos poucos debates, as ideias que so objecto de discusso internacional -- as questes relacionadas com a realidade virtual, a com o ps-humano, ou com as auto-estradas informticas -- no se discutem em Portugal. +Por outro lado, no h radicalismo. +Sem radicalismo no h qualquer evoluo da sociedade. +P. -- Uma questo bvia: em ano de eleies, e embora o debate sobre a situao portuguesa seja feito em Fevereiro, a iniciativa no se colar demasiado s foras polticas -- socialistas-Plataforma de Esquerda -- que esto frente da Cmara de Cascais? +Apesar dos elevados montantes que o Banco de Portugal tem vindo a injectar no sistema ao longo do actual perodo de constituio de reservas de caixa, a sesso de ontem do mercado monetrio interbancrio voltou a apresentar-se bastante pressionada pela procura de fundos, com os primeiros nveis de oferta no curto prazo a serem superiores ao fecho da sesso de ontem. +H medo no Sul dos Estados Unidos. +As igrejas frequentadas pela populao negra esto a ser incendiadas e os lderes das comunidades acreditam que so manobras de intimidao dos que sonham com o regresso da supremacia branca. +Casos como o da Sopete, Lusotur ou Aliana Seguradora ilustram de forma pouco favorvel a segurana que os investidores podem ter nas previses adiantadas por algumas empresas ... +Na Sopete, os resultados alcanados em 1991 no chegam sequer para cobrir os dividendos de 125 mil contos estimados no primeiro semestre, isto apesar de no comentrio ao balano do primeiro semestre de 1991 ser referido que se aponta para o segundo semestre um real crescimento da actividade do todo da empresa, e em especial da rea do jogo e da rea hoteleira. +Enfim, e esse ter sido o ponto decisivo da sua interveno, Antnio Tanger sublinhou que a ocorrncia de um novo problema poderia criar a Portugal algum embarao diplomtico. +No quadro poltico actual, em que Portugal ocupa a presidncia das Comunidades, era de todo indesejvel que a diplomacia portuguesa tivesse de intervir para protestar junto de qualquer pas dessa regio. +Isso poderia revestir-se de particular gravidade se o ataque ocorresse dentro de Angola, onde h poucos dias dois padres e uma freira foram mortos num ataque de bandidos. +Aps esta interveno, os membros da expedio reuniram-se com a organizao da viagem para deliberar sobre o que fazer. +Foi na sequncia desse encontro que Pedro Villas Boas decidiu dar por finda a expedio, inviabilizando desta forma o prosseguimento da viagem anteriormente definido por uma pequena coluna expedicionria. +Wayne Barker, cineasta arborgene da Austrlia, fez a apologia duma cultura viva, que conserve a herana dos nossos pais mas com os olhos postos no futuro. +Walter Saunders, outro realizador e produtor arborgene, sublinhou a pilhagem cultural que a sua comunidade continua a sofrer, por exemplo no domnio das imagens: dos motivos arborgenes utilizados para ornamentar loua e tecidos, os smbolos que ornam os avies australianos, os boomerangs fabricados e vendidos por australianos brancos, sem que os arborigenes recebam quaisquer direitos .. + um disco que estava j meio feito no Canad com canes compostas antes do 25 de Abril. +Acabou por ter uma vertente mais poltica em consequncia dos acontecimentos imediatos, por uma questo de sensibilidade linguagem da poca e da vontade de participar nela. +(...) Mas a pesquisa formal basicamente a mesma. +Podem dizer-se coisas muito vlidas politicamente com uma linguagem pobre e que no entra nas pessoas. +Cecilia Bartoli cantou em So Carlos, em Lisboa, no passado 24 de Maro. +No numa pera de Rossini, como reclamaria a misso do teatro, no ano do bicentenrio. +Mas oferecendo um conjunto de peas para voz e piano do compositor, numa memorvel demonstrao da vitalidade da redescoberta do canto rossiniano. +Se h esttica de canto que foi deteriorada quase irreversivelmente com os novos conceitos de drama musical que imperaram durante a segunda metade do sculo passado e a primeira metade do presente, essa foi certamente a perseguida por Rossini. +Tourabi foi tambm o artfice de uma aliana nica, entre um pas rabe sunita -- o Sudo -- e um pas no rabe xiita -- o Iro, selada quando o Presidente iraniano, Ali Akbar Hashemi Rafsanjani, visitou Cartum em Dezembro de 1991. + impossvel saber se foi Rafsanjani quem manipulou Tourabi, ou vice-versa, mas seguro que Sudo obteve vantagens polticas e financeiras, porque se transformou no coordenador de vrios movimentos islamistas, observou Balta, enumerando a FIS, na Arglia, a An-Nahda, na Tunsia, a Irmandade Muulmana, na Jordnia, o Hezbollah, no Lbano, a Gama'at Islami, no Egipto, e o Hamas, na Faixa de Gaza -- uma internacional de barbudos. +A faixa etria e as horas do dia mais perigosas coincidem em todos os distritos, mas Beja foi o que registou menor incidncia de condutores alcoolizados em Dezembro: apenas 2,4 por cento. +Durante o ltimo ms do ano passado a Brigada de Trnsito aplicou o teste de alcoolmia a 25.093 condutores, em todo o continente e detectou 1.578, ou seja, 6,2 por cento do total, que conduziam sob o efeito do lcool. +No foi por isso, contudo, que a Vidisco se sentiu menos ofendida, de modo que acabou por decidir meter o caso em tribunal, porque aquilo no se faz. +A televiso paga por todos ns, para qu? +Para eles fazerem publicidade privada, como se fosse um programa pago? +No digno de um programa de um organismo oficial, que na verdade de uma associao de multinacionais. +Tentmos encontrar logo um advogado que estivesse dentro da legislao, o que foi difcil por causa da quadra de festas. +Agora, temos um advogado e se ele considerar que h suficiente matria legal para isso, vamos a tribunal. +Resta saber quem poder ser processado. +Mas ser que vai ficar por aqui? +O que houve entre a primeira e a segunda metade desse ano parece, assim, uma espcie de intervalo -- um tempo fora da Histria. +Enquanto o possvel medo de represlias fazia com que as acusaes dos jogadores contra Octvio nunca se tivessem concretizado, ele, em contrapartida, teve a coragem de dar a cara em alguns momentos de grande polmica. +Assim, no seu primeiro Porto-Sporting como adjunto, com a rivalidade Pinto da Costa-Joo Rocha estava no auge e com Artur Jorge a no conseguir ganhar nas Antas, Octvio seria acusado de agredir cabeada e a murro alguns responsveis sportinguistas, indo assim mais alm do que o prprio treinador principal que, impvido e sereno, comandou a sua equipa sem se envolver nas guerras marginais em que Pinto da Costa e Octvio mostravam estar como peixes na gua. +Quando Quinito foi contratado para o FC Porto e se props recuperar, entre outros, Gomes e Madjer, o homem de Palmela afastou-se e, nas suas costas, comearam a aparecer muitas das histrias a seu respeito que, at ento, tinham ficado escondidas nos segredos dos bastidores. +Mark Miller o treinador-jogador, mas possvel que Toni Formosa venha a ser uma espcie de manager da equipa, com assento no banco, se aceitar a proposta que lhe fez o presidente do clube. +Ter que dar uma resposta at prxima semana. +Nos jogos com o Ekranes, os dois golos foram marcados pelo lbero Buttigieg, um jogador elegante e talvez o melhor do pas. +Os outros internacionais so Cluett (guarda-redes), Caucchi e Buttigieg (defesas) e Buhagiar (normalmente defesa-esquerdo). +R. -- Parecer-me-ia lgico, num pas pobre, de recursos escassos como o nosso, que, no mnimo, as famlias e os estudantes suportassem uma percentagem correspondente dos ingleses. +P. -- O que iria tornar o ensino superior um ensino mais elitista, dos ricos. +Abriu ontem ao pblico a primeira iniciativa da galeria do Instituto de Arte Contempornea (IAC). +No Pavilho Branco do Museu da Cidade, em Lisboa, a exposio de Adriana Varejo inaugura uma nova rea de interveno do IAC. +Os objectivos polticos so clarificados no texto do director: criar um local de reflexo, enfatizando a cidade como sede do pensamento e de problematizao de temas que fazem parte do nosso tempo, num campo que , nesta fase, dedicado multiplicidade de formas e conceitos que se originam a partir do conhecimento de culturas e geografias que nos so de menor adjacncia. +Assim, a galeria pretende acolher imagens ligadas ao que se convencionou designar por multiculturalismo. +A inteno descentralizadora do programa Rotas, assumido pelo Ministrio da Cultura e que o IAC integra, pois confrontada com uma iniciativa tendente a centralizar a discusso artstica e a capitaliz-la. +Iniciar a programao atravs da apresentao da obra de Adriana Varejo uma opo politicamente perfeita: porque uma artista brasileira, porque uma artista que recorre (entre outros) ao manancial infinito das imagens deixadas no Brasil pelo colonialismo portugus, porque est integrada nos circuitos mais dinmicos da internacionalizao artstica actual. +Adriana Varejo situa-se no campo das reflexes sobre o cruzamento de culturas. +Nesse sentido, cruza imagens dos diferentes tempos e civilizaes, criando imagens de sntese e procurando problematizar, atravs do objecto artstico contemporneo, os resultados de uma experincia histrica universal. +Um catlogo com textos de Isabel Carlos (do prprio IAC) e Paulo Herkenhoff (comissrio para a Bienal de So Paulo) completa a apresentao. +Leonor Beleza, presidente da mesa do Congresso do PSD, foi convidada a colaborar com o Governo no debate sobre as reformas do sistema eleitoral. +Os jornalistas tinham perguntas agressivas para o Presidente, mas ficaram algo desarmados com o ar triunfal de Clinton. +Um ainda indagou se ele tencionava mesmo lanar a invaso, contra a opinio da maior parte dos americanos e do Congresso. +Clinton, que afinal de contas no teve de lanar invaso nenhuma, deu exemplos de outras intervenes militares, como em Granada ou no Panam, em que os Presidentes no pediram autorizao ao Congresso. +Nem todas as decises podem ser populares. +No se pode conduzir a poltica de acordo com as tendncias das sondagens. +No creio que o povo americano queira que eu fizesse isso. +como se pode dizer aos americanos que Cdras um crpula e dois dias depois afirmar que se trata de um honrado militar? +Clinton balbuciou qualquer coisa sobre a inutilidade dos ressentimentos e vinganas, citando Jean-Bertrand Aristide, o Presidente exilado que dever, at 15 de Outubro, regressar ao Haiti, e de quem a CIA diz ser um doente mental com perigosas ideias anti-americanas. +O mesmo documento afirma que a forma jurdica e os critrios de gesto nela implcitos se revelaram inapropriados misso que foi cometida ao Teatro de So Carlos, quando da sua transformao em EP em1980: organizar de forma permanente espectculos de msica, pera e bailado e para os divulgar pelo pas, para dar a conhecer as obras de autores nacionais e para formar e manter um corpo de cantores / actores e de msicos. +Ao longo de quase 200 anos, a histria do Teatro Nacional de So Carlos acompanhou a vida cultural portuguesa, sendo imperioso criar condies para que, no novo renascimento que Portugal atravessa, as estruturas da rea da cultura se adequem ao acrescido dinamismo da sociedade. +Em concluso, ser de natureza privada a entidade responsvel pela nova gesto do Teatro Nacional de So Carlos, diz, a terminar, o comunicado. +Os Missionrios do Esprito Santo vo alargar cidade do Porto o trabalho que j desenvolvem na regio de Lisboa, de apoio aos imigrantes lusfonos que residem em Portugal. +A deciso dever ser concretizada a curto prazo, depois de o captulo provincial (assembleia) daquela congregao religiosa ter debatido a aco do Centro Padre Alves Correia (Cepac), de Lisboa -- o organismo, da responsabilidade dos missionrios, que esteve em diversas aces de apoio legalizao dos imigrantes clandestinos, e que denunciou a corrupo existente na delegao de Faro do Servio de Estrangeiros e Fronteiras, investigada pela Polcia Judiciria (ver PBLICO 20/07/94). +Nesse sentido, eu gostaria de ver alargado o trabalho do Cepac a cidades como o Porto, porque ele um sinal de ateno misso no nosso pas. +Da ltima vez em que estivemos em Paris nossa casa foi um apartamento na Rue de la Pompe, no 16 arrondissement. +Uma alternativa mais barata para hotel, com a vantagem adicional de nos permitir simular uma certa domesticidade francesa. +Viajamos para fugir da nossa rotina mas somos seduzidos pela rotina dos outros e ter um lugar certo onde comprar a baguette para o caf da manh passa a ser um indispensvel prazer parisiense. +Conhecer no a vida mas a vidinha de um lugar depende de estabelecer-se na comunidade por alguns anos. +Durante dez dias passamos vrias geraes na Rue de la Pompe. +Na nossa vizinhana imediata existiam quatro agncias funerrias e duas lojas especializadas em queijos. +A concentrao de funerrias se deve, imagino, proximidade do cemitrio de Passy. +Lojas s de queijos so comuns em Paris e uma experincia turstica interessante e barata entrar numa delas e respirar fundo. +Voc sai com a certeza de que chegou muito perto do corao selvagem da Frana ou ento de uma cura definitiva para a sinusite. +A coincidncia de funerrias e queijarias na nossa circunstncia no significava nada, portanto, mas no pude escapar da mrbida observao de que estvamos cercados pelas pompas da morte na Rue de la Pompe. +O queijo tambm a encomendao cerimonial de um morto, no caso uma poro de leite. +James Joyce chamou o queijo de defunto do leite, e a qualidade de um queijo o resultado da aco de bactrias vivas num corpo morto. +Ou seja, de apodrecimento controlado. + o que acontece tambm na preparao de carnes secas e aves e peixes faisandes e no correcto acondicionamento de corpos humanos para a eternidade. +os dois assimilam o facto que a vida uma doena incurvel com uma taxa de mortalidade de 100 por cento, e se tornam melhores por isso. +Rodrigo Mil Homens venceu ontem, na classe Runabout Limited, a segunda etapa do Europeu de Jet Ski, que durante o fim-de semana se disputou no lenol de gua do Rio Lima, na praia do Prior, em Viana do Castelo. +Miguel Valente, por seu lado, foi o vencedor na classe Sport Limited. +Estes foram os nicos portugueses a conseguir um primeiro lugar nas vrias classes que fizeram parte desta etapa do Europeu, que contou com a participao de 186 concorrentes, 28 dos quais portugueses. + Finalmente, apesar de j ter sido editado em 1992, merece referncia o clssico norte-americano Spiderman -- o Homem-Aranha --, uma criao original de Steve Dikto (desenho) e Stan Lee (texto), que evocado por ocasio do 30 aniversrio da sua criao (1962) numa edio em comic-book de luxo pela editora espanhola Forum (Planeta-De Agostini). +La Saga del Traje Alienigena tem a assinatura de Ron Frenz, Ruck Leonardi (desenho), Tom DeFalco e Roger Stern (argumento). +BB -- Em geral? +[ Risos ...] A funo da arte a de expandir as possibilidades da linguagem visual. +Por outras palavras, o que a arte nos d mais possibilidades de ver as coisas de diferentes maneiras. +E creio que o bastante. +O mesmo se passa em relao literatura. +Acredito que a funo da literatura e da arte a de expandir as possibilidades do que as pessoas podem dizer, mas no necessariamente para fazer delas melhores seres humanos. + possvel que tenha essa consequncia, mas no se deve misturar as coisas. +Quais que so as prioridades? +O fundamental, para mim, que a arte seja livre de constrangimentos. +P -- H uma nova moral nos Estados Unidos? +J presente no local sob os auspcios das Naes Unidas para garantir o respeito das zonas de excluso area decretadas na Bsnia, a Aliana Atlntica no dever encontrar grandes dificuldades no alargamento da sua misso. +Mas, apesar da imagem de coeso que ontem se esforaram por exibir durante uma reunio no Luxemburgo, os ministros dos Negcios Estrangeiros da CE e o secretrio de Estado norte-americano, Warren Christopher, no esconderam algumas nuances sobre a estratgia a seguir face Bsnia. +No existe, pois, um conceito operacional de urgncia, como bem atestam as urgncias dos hospitais. +1 Prioridade -- situaes exigentes: situaes mais ameaadoras para a vida e que necessitam de interveno instantnea. +Exemplo: fractura da laringe com obstruo respiratria completa. +2 Prioridade -- situaes emergentes: situaes que exigem interveno imediata num perodo de poucos minutos. +Exemplo: pneumotrax de tenso [ ar dentro da cavidade pleural a comprimir os pulmes e o corao, eventualmente ]. +3 Prioridade -- situaes urgentes: situaes que necessitam de interveno dentro da primeira hora. +Exemplo: hemoperitoneu devido a hemorragia intra-abdominal contnua [ leso dentro da cavidade abdominal a sangrar continuamente ]. +4 Prioridade -- situaes deferveis: situaes que podem ou no ser imediatamente evidentes, mas que necessitaro de tratamento subsequente. +Outro aspecto que suscita preocupao o uso, por vezes indiscriminado, de antibiticos. +Entre as resistncias a estes frmacos provocadas por este tipo de actuao -- por parte dos profissionais e da populao --, inclui-se, como um dos exemplos mais graves, a ineficcia dos medicamentos contra a tuberculose, doena cuja incidncia continua a deixar Portugal muito mal colocado no panorama europeu. +A emergncia da sida, qual est associada uma srie de infeces oportunistas, o problema da hepatite C (recentemente identificada e ainda sem cura nem tratamento) e o recrudescimento da papeira (mais de onze mil notificaes durante o ano passado) constituem outras das facetas mais negativas da situao portuguesa em matria de doenas transmissveis. + Mas a margem de 87 para 10 a favor da Lei de Reautorizao dos Institutos Nacionais de Sade, de que fazia parte a clusula respeitante ao tecido fetal, fez renascer as esperanas, entre muitos dos interessados em prosseguir as pesquisas envolvendo tecido fetal, de que esta medida tenha fora suficiente para ultrapassar o veto presidencial. +A votao representa tambm uma vitria significativa para os que se propem efectuar este tipo de investigao, uma vez que conseguiram convencer muitos senadores antiaborto que defender a utilizao de restos fetais no a mesma coisa que defender a prtica que lhes d origem. +Voltando aos terrores da Lapa dos Morcegos: fartos de tantas vtimas, os aldees decidem armar um cavaleiro capaz de defrontar a fera. +Terrvel foi o combate e assustadores os gritos da batalha, que o improvisado cavaleiro acaba por vencer, expulsando a fera, que foge por entre o pblico e vai aterrorizar outras paragens. +De entre os que assistiam a esta pea da companhia Aquilo, as crianas eram, sem dvida, as mais fascinadas. +Algumas choravam, aterrorizadas, outras respondiam aos urros do monstro, e um petiz chegou mesmo a propor aos pais uma incurso na gruta, para mat-la, fera, pois ento. +No a desvalorizao que vai resolver os problemas de fundo do sector txtil. + assim que Jorge Seabra, gestor txtil, avalia a recente desvalorizao da moeda portuguesa que, no entanto, considerou insuficiente. +Para Jorge Seabra, a questo tem de ser posta a dois nveis. +Por um lado, a efectiva perde de competitividade dos produtos portugueses face a produtos concorrentes provenientes do espao europeu, em especial da Itlia e da Espanha. + nesse nvel que, em sua opinio, a desvalorizao foi insuficiente. +Mas, por outro lado, no a desvalorizao do escudo que vai resolver o problema de fundo de competitividade da indstria portuguesa. +Uma autocombusto ocorrida ontem de manh em depsitos da empresa de tratamentos pretolferos Nesta, em Sines, no chegou a provocar ferimentos nos operrios nem prejuizos avultados. +A Sociedade Lisboa 94 poder vir a desvincular-se do apoio, de 25 mil contos, produo da pera O Corvo Branco, de Philip Glass e Bob Wilson, com libreto de Lusa Costa Gomes, encomendada pela Comisso Nacional para as Comemoraes dos Descobrimentos (CNCDP). +Marcada inicialmente para o fim da presidncia portuguesa das Comunidades, em Junho de 1992, depois anunciada para Abril de 1994, no Centro Cultural de Belm (ver PBLICO de 10/2/1992) a pera s poder ser apresentada em Janeiro / Fevereiro de 1995. +Vasco Franco, vereador responsvel pela PM lisboeta, contesta e salienta que a aco da polcia alfacinha mais importante do que parece primeira vista. +Alm do controle da venda ambulante, os efectivos acorrem s situaes de catstrofe, fiscalizam bairros degradados, para evitar o aparecimento de novas barracas. +Muitas vezes, a PM a nica autoridade que passa em determinados bairros. +S este ano, at Setembro, a polcia alfacinha j retirou da via pblica 1600 veculos abandonados, misso essencial numa cidade com pouco estacionamento, sublinha. + Alm disto, ainda acompanham as aces de despejo. +A verdade que se tratam de tarefas executadas noutros municpios por fiscais camarrios. +Mas, com menos eficcia que a PM, assegura Vasco Franco. +Entretanto, Couto Ferreira apresentava ontem um alto teor de cido rico no sangue e encontrava-se em acelerado emagrecimento, segundo informou o director-clnico do Hospital Prisional S.Joo de Deus, Manuel Pinu. +Por agora, as alteraes registadas ainda no so irreversveis mas a presena de um elevado teor de cido rico no sangue pode ter consequncias a nvel renal, frisou o citado mdico. +Couto Ferreira encontra-se a ser acompanhado pelo mdico mas recusou qualquer tipo de tratamento, caso entre em coma. +O lder do PSD-Porto, Lus Filipe Menezes, desafiou ontem o presidente da Cmara do Porto a assumir o mandato de deputado durante a discusso do Oramento de Estado para 1998 para que prove o seu empenho na defesa da regio, mas Fernando Gomes, em jeito de comentrio, passou ao lado do repto. +s picardias e jogos de palavras do doutor Lus Filipe Menezes no tenho rigorosamente nada a dizer, porque tenho mais com que me preocupar, disse. +Este grupo religioso j foi visitado pelas autoridades policiais, em fins de Novembro, no seguimento de queixas em matria de urbanismo, contra esttuas de dimenses desmesuradas erguidas perto do mosteiro. +Os cavaleiros foram, inclusive, proibidos de construir um templo pirmide de propores megalmanas. +Entretanto, nos Estados Unidos, os sobreviventes do desastroso ataque policial contra os membros do culto davidiano, o massacre de Waco, juntamente com os familiares das vtimas mortais, vo processar o presidente norte-americano, Bill Clinton, e o FBI. +O exerccio do poder nas organizaes , por vezes, descrito em termos de jogos e de jogadores. +Os autores de The Strategy Process referem o especialista em cincias polticas Graham Allison, que, em 1971, descreveu os jogos nas organizaes e nos governos como complexos, subtis, simultneos e sobrepostos. +a coligao de regras, em cada momento define o jogo. +Mintzberg e Quinn identificam uma srie de jogos de poder, de entre os quais retirmos e adaptmos os que apresentamos no quadro junto. +O essencial, na compreenso dos jogos de poder, conseguir saber quem est envolvido e onde est a fora em cada situao concreta. +A gesto de topo -- alvo a atingir em muitos destes jogos -- pode ganh-los se os conhecer bem e souber jog-los. +23 de Maio -- Manifestaes espontneas em protesto contra o facto de Daniel Cohn-Bendit, lder do Maio de 68, ter sido proibido de residir em Frana. +A Unio Nacional dos Estudantes de Frana, o sindicato dos Professores do Ensino Superior, o Movimento 22 de Maro e os Comits de Aco Liceal convocam uma manifestao para o dia 24 em solidariedade com Cohn-Bendit. +cerca de 30 mil jovens manifestam a sua solidariedade com Cohn-Bendit. +s 20 horas, o general De Gaulle fala ao pas, propondo a realizao de um referendo. +Os manifestantes reagem interveno, agitando lenos brancos e gritando Adeus De Gaulle. +O chefe do Estado-Maior da Fora Area, general Mendes Dias, decidiu proceder a algumas alteraes nos quadros das Oficinas Gerais de Material Aeronutico (OGMA), tendo sido determinada, na passada sexta-feira, a substituio do general Rui Espadinha, na direco daquele estabelecimento fabril. +Interinamente, o brigadeiro Portela ir assegurar a chefia das OGMA, mas, nos prximos meses, dever suceder uma reestruturao mais profunda. +Seria bom que os lderes partidrios pr-referendo fossem mais claros expondo razes objectivas, como por exemplo as de foro constitucional, tico ou consuetudinrio. +Os cidados gostariam de saber porque se deseja retirar aos deputados o direito que tm de aplicar a sua sapincia e poder na feitura de leis que a todo o povo e ao pas dizem respeito. +E tambm gostaramos de saber porque se exige a transferncia de tais poderes para o veredicto do povo, sendo sabido que se trata de assuntos de elevada complexidade poltica, social e jurdica, para os quais o cidado comum tem menor preparao tcnico-cientfica. +A investigadora acha mesmo que no h razes para se excluir as protenas animais das raes, desde que exista uma certificao da origem dessas protenas que garanta que elas provm apenas das partes no perigosas das carcaas. +Antes de 1988, a contaminao era imparvel e, hoje, estamos a tomar precaues excessivas numa altura em que o risco j muito mais pequeno. +E diz que, daqui a uns anos, a BSE, que era uma doena rara at h dez anos, tornar a ser novamente isso: uma doena rara. +A guerra entre os jornais desportivos est no ponto de rebuado. +A Gazeta, que passa a ser editada cinco dias por semana, parece em vias de alcanar o seu objectivo: obrigar o arqui-rival A Bola a tornar-se um jornal dirio, o que, segundo P&N apurou, ser um facto a partir de Janeiro do prximo ano. + E, de caminho, instalou-se na redaco de A Bola um clima de autntico PREC, com a demisso do chefe de redaco, Joaquim Rita, e a venda, por parte de Aurlio Mrcio, da quota que detinha no jornal. +Mrcio amealhou 70 mil contos e est de malas feitas para o Record, acompanhado, na sua transferncia, por Joo Alves Costa e Norberto Santos. + Quem disse que as transferncias eram s no defeso? +J.M. -- A que est. +Temos prestado uma ateno diminuta interaco entre a realidade fsica, o problema ambiental e o sistema poltico. +Se pegarmos no caso de frica, a grande quantidade de gente que ali morre de fome no resulta do problema ambiental ou de uma agricultura subdesenvolvida, mas dos governos. +So pases mal geridos que, por isso, no conseguem fazer chegar os alimentos aos seus povos. +Curiosamente, na Amrica do Sul, que dantes estava bem pouco desenvolvida, as populaes conseguem bastar-se em alimentos e vivem da exportao de alguns produtos. +Portanto, voc deveria preocupar-se com os sistemas polticos vigentes. +E.G. -- Continuo a no estar convencido de que se trate apenas de um problema poltico. +Porque a produo por hectare em frica tem diminudo drasticamente, o campo est a desertificar-se. +O lenol fretico baixa todos os anos, as pessoas tm de cavar poos cada vez mais fundos. +Muita dessa desertificao tem sido disfarada com o uso crescente de adubos, no tanto em frica como na ndia e noutros stios. + uma causa macia de subnutrio e fome. +Na semana passada, Fidel Castro avisara que se Washington no alterar a poltica quanto aos emigrantes cubanos poder ficar sujeita a uma inundao, pois que a Havana deixar de tentar impedir os seus cidados de partir e os parentes de os virem buscar. +O Presidente levantou o espantalho de um xodo como o de 1980, ano em que 125 mil cubanos se dirigiram de barco para os Estados Unidos, onde tantos dos seus compatriotas j se encontravam a viver no estado da Flrida. +Os bombardeamentos podem produzir uma reviravolta na guerra que ameaa, seno a integridade territorial do pas pelo menos a estabilidade do regime de Mobutu Sese Seko que o domina h mais de trs dcadas, admitiram analistas citados pela agncia francesa. +Mas so escassas as informaes sobre as vrias frentes de combate, desconhecendo-se at que ponto as foras governamentais progridem no terreno. +S h rumores, por exemplo que os militares tomaram, com a ajuda de antigos soldados ruandeses (hutus) e mercenrios, posies perdidas h dez dias na estrada para sudeste, para Walikale, e a cidade de Bafwasende, a Leste, e que a aviao se prepara para atacar Punia, situada entre Kisangani e Bukavu. +Assim, o projecto de lei ontem aprovado por unanimidade na Assembleia da Repblica, em vez de afirmar que as taxas ficam suspensas at ao fim do ano (como o texto inicial, da iniciativa do social-democrata Rui Rio, previa), passou a estabelecer como limite para a suspenso a entrada em vigor do diploma que regule a utilizao de cartes de dbito de pagamento automtico. +E Rui Carp, o vice-presidente da bancada laranja responsvel pelas questes econmicas, foi claro ao afirmar no plenrio que, num curtssimo perodo de tempo o Governo dever deixar esta questo resolvida. +Outras das alteraes introduzidas no projecto de lei diz, alis, respeito iniciativa legislativa que inevitavelmente se seguir. + que, enquanto o primeiro projecto afirmava que uma vez suspensa a taxa, a Assembleia da Repblica apresentaria um projecto de regulamentao do uso dos cartes, o projecto ontem aprovado apenas afirma que dever ser aprovada at 31 de Dezembro do corrente ano legislao que preencha cabalmente o vazio legislativo existente. +Ou seja, no ser necessariamente o Parlamento a faz-lo, e a convico dominante na direco da maioria que, a partir de agora, o Governo ir ter uma participao decisiva. +A teoria diz que necessria a existncia de uma certa distncia entre o charme e o anticharme para o Psi ser produzido. +Se ns cumprimimos a matria, no deixamos os quarks distanciarem-se o suficiente para produzirem o Psi, explica Paula Bordalo. +H uma distncia mnima para o Psi se formar e teoricamento se no se conseguir observar o nmero suficiente de Psis em relao fsica normal, porque podemos estar na presena do plasma, continua. +Desde as vrias recolhas de dados com a experincia do oxignio, passando pelo enxofre e agora com o chumbo, o detector de mues -- a partcula em que o psi se decompe e que os fsicos procuram para saber se o psi foi produzido -- foi sempre mostrando uma diminuio do psi. +O zimbabweano admitiu mesmo que Sampras dos poucos jogadores que costuma observar em aco. +Ele to suave. +Todas as suas pancadas so quase perfeitas, justificou. +Com esta presena nos quartos-de-final, o seu melhor resultado em provas do Grand Slam, Black angariou pontos suficientes para subir cerca de 30 lugares no ranking, mas no s isso que o tenista africano leva de Flushing Meadow. +O mais importante saber que sou capaz de bater jogadores do ' top-10 ', afirmou ele, referindo-se eliminao neste Open de Michael Stich (8 ATP) e Thomas Enqvist (9 ATP). +Mas o dia j tinha comeado bem para as cores norte-americanas, com o triunfo de Meredith McGrath e Matt Lucena na final de pares mistos. +O conjunto norte-americano derrotou a tambm norte-americana Gigi Fernandez e o checo Cyril Suk por 6-4, 6-4, conquistando o ttulo e o cheque de 7500 contos. +Tnhamos decidido desde o primeiro encontro que amos divertir-nos e que no ficaramos zangados se as coisas corressem mal, afirmou Lucena, que tambm revelou que os dois concordaram em jogar apenas cinco dias antes de a competio se iniciar. +McGrath j tinha sido finalista nesta variante em 1989, o ltimo ano em que o ttulo tinha revertido tambm para um par da casa. +Segundo Artur Moreira, a notria leu a escritura e s faltava assinar. +Depois, continua, disse que s o faria na presena de duas testemunhas. +No dia seguinte, Armanda e Moreira voltaram ao cartrio, acompanhados das testemunhas. +Sem qualquer explicao, a notria disse que no fazia a escritura, recorda o vendedor. +E as testemunhas no chegaram a ser ouvidas. +Seguiu-se uma troca de palavras menos amistosa e Elvira Maris acabaria por chamar a polcia, declarando ter sido maltratada e que o bom funcionamento do cartrio havia sido perturbado. +A autoridade chegou e procedeu identificao dos dois indivduos. +O que eu pergunto por que que para a polcia a minha identificao suficiente e para a senhora notria no , questiona Maria Armanda. +Eu estou a ser alvo de discriminao. +Ela est a brincar comigo, com o vendedor, com as testemunhas, com todos ns, conclui. +Por sua vez, Moreira afirma conhecer outro travesti que comprou uma casa j h 18 anos, sem ter tido qualquer problema. +Contactada pelo PBLICO, Elvira Maris declarou no poder fazer uma escritura quando aparece uma pessoa com uma identidade diferente da presente no documento comprovativo da sua identidade. +A Alemanha no lhes concedeu asilo e os seus pases muitas vezes no os querem de volta. +So detidos, para evitar tentaes de clandestinidade. +Nas prises de deportao alems, esto cerca de quatro mil indesejados espera da expulso. +At l, podem passar 18 meses em celas superlotadas. +Sem terem cometido qualquer crime. +Sem nada poderem fazer. +A no ser esperar pelo destino de quem nasceu no pas errado. +No incio do sculo, era uma esquadra de polcia. +As celas tinham capacidade para albergar at 140 detidos por um curto perodo de tempo. +Perante as novas necessidades, o velho edifcio na Kruppstrasse foi reciclado. +Hoje a mais conhecida priso de deportao berlinense. +Portugal uma rebaldaria. +E como no h Constituio, ento vamos todos tomar atitudes arruaceiras. +De bandeira branca ao ombro, reclamando o encerramento das grandes superfcies ao domingo, o comerciante, de face congestionada, resumiu sua maneira a inteno dos 150 lojistas presentes na reunio realizada anteontem noite, em Vila Nova de Gaia. +Depois de discutidos o melhor dia e a melhor hora para a manifestao, a maioria pronunciou-se pelo ajuntamento no prximo dia dois de Maro, sbado, a partir das quatro horas da tarde, na sala de visitas do Porto: a Praa General Humberto Delgado. +Um falso mdico passeou-se durante pelo menos dez anos pelo Hospital de Santa Maria. +O doutor Dinis, segundo a Polcia Judiciria, chegou a dar consultas no estabelecimento de sade lisboeta e a encaminhar pacientes para especialistas a quem convenceu da condio de colega. +O director de Santa Maria admitiu ontem ter sido detectado um indivduo que se fazia passar por mdico, imediatamente entregue s autoridades, mas garantiu desconhecer que ele desse consultas. +O deputado do PSD Guilherme Silva esteve reunido h dias com as estruturas sindicais dos juzes, a quem comunicou o facto de o seu partido, apesar de estar sensibilizado para as queixas dos juzes, apenas tencionar desbloquear a situao num contexto em que se gere na Assembleia da Repblica um amplo consenso democrtico, designadamente com a adeso do PS que, segundo a direco parlamentar do PSD, se tem mostrado fechado a considerar o assunto numa perspectiva de regime. +Esta perspectiva de regime de que fala o PSD implica a busca de uma soluo no quadro do Estatuto Remuneratrio dos Titulares dos Orgos de Soberania, e no apenas uma alterao pontual deste diploma com base na interpretao legal avanada pelo Sindicato dos Magistrados do Ministrio Pblico. +Acredito que se referissem a animais indefesos, pois o termo cobardia no se aplica a quem enfrenta toiros. +Apresenta esses dois grandes escritores -- por convenincia --, mas olvida nomes importantes no mundo das letras e das artes. +No lhe diz nada Ernest Hemingway, Prmio Nobel da Literatura, um homem cativado pela festa e que escolheu a terra de Espanha para sua ltima morada? +E Federico Garcia Lorca, que deixou to belas obras e alguns sentidos poemas dedicados a um famoso toureiro? +Esquece Picasso, Goya, Mariano, Benlliure, que honraram a festa com as suas pinturas e esculturas? +Quantos artistas, quer estrangeiros quer nacionais, a festa de toiros tem motivado. +o cinema nasceu em Portugal? +E o football? +Contudo, somos capazes de fazer isso tudo e bem. +Por fim, restar um lote de aces que ser vendido na Bolsa de Lisboa. +Os preos das aces para investidores nacionais sero mais vantajosos comparativamente aos valores para os no residentes. +No Ministrio da Indstria existe o desejo de manter o mximo de capital possvel em mos nacionais. +A justificao que este um sector em que podemos dar cartas, pelo que no convm uma alocao excessiva de poder de deciso para o exterior. +No entanto, por definir continuam as parcelas exactas de capital que sero alienadas em Portugal e no estrangeiro. +O Governo ainda no delineou os contornos definitivos da operao, o que dever acontecer no final de Abril ou princpio de Maio em Conselho de Ministros. +Actualmente, uma das questes colocadas pelos investidores a de qual ser o preo a pagar por cada ttulo. +Mas tambm aqui ainda nada est definido. +No entanto, o PBLICO soube que foram realizadas duas avaliaes da empresa de celulose. + Uma pela dupla BFE/Salomon Brothers e outra pelo BPI em associao com a UBS (Union des Banques Suisses). +O II Encontro Nacional de aquarofilia iniciou-se ontem com a inaugurao de exposies de filatelia, aqurios, artesanato temtico e fotografia, estando ainda includas no evento outras iniciativas, como workshops sobre fotografia e montagem de aqurios, animao musical e projeco de filmes. + Esta tarde tem incio o colquio, que conta com a participao de especialistas nacinais e estrangeiros em aquariofilia e actividades subaquticas, bem como responsveis por alguns aqurios europeus e o director do Oceanrio da Expo'98. +A pensar nos mais novos, o Programa Aquajnior permite a utilizao de programas multimdia relativos vida marinha. +O Porto tem um novo grupo de teatro que obedece ao princpio da diversidade. +A troca de experincias com outros grupos, a produo de espectculos, a organizao de debates e de ateliers de formao contam-se entre os objectivos do grupo, fundado por quatro actores profissionais. +O primeiro workshop, orientado por um especialista japons em commedia dell'arte, Kuniaki Ida, comeou esta semana. +A agitao volta das filmagens suscitou igualmente um novo interesse por aquele romance. +A Ilhu Editora teve de pr nas bancas mais uma edio cabo-verdiana -- a terceira desde que a obra conheceu a luz do dia --, de modo a satisfazer a curiosidade dos novos leitores deste primeiro e bem sucedido livro de Germano Almeida, cuja bibliografia no cessa de aumentar desde que decidiu lanar-se nesta tarefa de dar corpo s histrias e que, segundo ele, vo ter consigo todos os dias no seu escritrio de advogado, no Mindelo. +Segundo Francisco Manso, os custos do seu filme devero situar-se entre os 250 mil e 300 mil contos, oramento que ele considera razovel para os padres portugueses. +Portugal, atravs do IPACA, RTP e vrios outros organismos pblicos e privados, assegura o grosso do financiamento, cabendo a parte restante ao Brasil e Cabo Verde, e ainda a entidades ligadas Unio Europeia. +Os juristas franceses esto algo apreensivos com o novo Cdigo Penal, que entra hoje em vigor e que substitui o velho Cdigo Napoleo de 1810, ao cabo de um trabalho que durou 18 anos. +Quando em todo o mundo caram os muros com que alguns tambm tentaram defender a irracionalidade dos seus princpios, eis que em Portugal se estabelecem quilmetros infinitos de aramados, que esquartejam o pas em talhes, onde uma minoria autorizada a explorar at ao extermnio o que a natureza a todos oferece. +Pelo caminho, com o conluio de uns poucos que ocuparam ou ocupam altos cargos na Administrao (e que envergonham muitos outros que honrosamente se batem por um adequado ordenamento cinegtico), tm ficado enxovalhados, espezinhados e chumbados, o direito propriedade privada, o interesse nacional e o direito vida. +Com uma viso facciosa e mope do que o desenvolvimento rural (j que as receitas da caa raramente ficam nos locais onde esta se pratica), continua a Secretaria de Estado da Agricultura e do Desenvolvimento Rural (SEADR) a empenhar-se em propalar sem quantificar os proveitos desta actividade. +Mas quanto perde o pas por cada ave de rapina ilegalmente abatida? +Quanto perde o pas por cada predador envenenado? +Quanto perde o pas por cada espcie rara morta? +Quanto gasta anualmente a JAE na reposio dos milhares de placas que anualmente servem de alvo a caadores sem escrpulos? +Em quanto contabilizar os prejuzos causados por centenas de ces de caa que so anualmente perdidos ou abandonados pelos caadores? +Em quanto ficaria a limpeza de toda a imundcie provocada na natureza pelos almoos e lanches que fazem parte deste ritual de matana? +Quem deve ser responsabilizado pela poluio dos nossos campos e guas, provocada pelas toneladas de chumbo anualmente disseminadas por milhares de disparos? +E as perdas de rendimento ao nvel regional, causadas pela eliminao, nas zonas de caa, das prticas agrcolas e pastoris consideradas incompatveis com as actividades cinegticas? +E os postos de trabalho assim eliminados? +Entretanto, durante toda a manh de ontem, uma testemunha tentou, na esquadra da PSP, dizer o que tinha visto e ouvido. +Mas esse direito foi-me sempre negado, afirma a testemunha, A. Carvalho. +O polcia disse-me que o agrediu, mas agora, horas depois, desmente tudo, adiantou. +O caso foi entregue PSP das Caldas da Rainha e remetido ontem ao tribunal, que decidiu que o condutor no pode sair da rea da residncia, embora possa conduzir, tendo sido instaurado inqurito para averiguaes. +Tambm em Outubro, quando a Cria Diocesana denunciou publicamente o fenmeno das profanaes, Aristides Lima, dirigente do PAICV, foi uma das vozes a levantar-se na condenao dessa prtica e a manifestar comunidade catlica a solidariedade do seu partido. +R. -- Nestes casos, a boa poltica que durma em casa. +Assim, ao menos sabe-se onde e com quem. +A vizinha do apartamento da frente insiste em fazer ioga nua, vista de todos. +Queixa-se ao senhorio? +O Teatro Experimental de Cascais apresenta Inventrios, no Auditrio Mirita Casimiro. +MONTEMOR-O-NOVO A Unidade de Infncia do Centro Dramtico de vora estreia hoje noite, no Cine-Teatro Curvo Semedo, O Meu Amigo Rodrigo. +Polticos israelitas e da Organizao de Libertao da Palestina (OLP) afirmam-se dispostos a punir os fundamentalistas islmicos que no domingo causaram 19 mortos numa povoao a norte de Telavive. +O ministro israelita dos Negcios Estrangeiros, Shimon Peres, afirmou ontem que o Governo de Yitzhak Rabin vai pressionar o lder da OLP, Yasser Arafat, no sentido de que se actue energicamente perante a Jihad Islmica, o grupo radical que reivindicou o facto de dois dos seus militantes suicidas -- naturais da Faixa de Gaza -- haverem sido os responsveis pelo atentado de Beit Lid. +Cristo ressuscitou? +Entre metade e um quarto dos estudantes catlicos acreditam em Deus mas no em Cristo. + um fenmeno de desintegrao social. +As consequncias so imprevisveis. +Voltando a mostrar-se particularmente vontade em condies de pouca aderncia, o brasileiro Ayrton Senna (McLaren / Ford) conseguiu ontem o melhor tempo na primeira sesso de qualificao para o Grande Prmio da Europa que amanh se disputa em Donington (Inglaterra). +No entanto, a luta pela pole position no est ainda terminada, com o francs Alain Prost (Williams / Renault) a prometer dar luta ao brasileiro, hoje, na derradeira sesso de qualificao. +Aquilo que os pilotos temiam confirmou-se, com um primeiro dia de treinos muito chuvoso, a dificultar as suas tentativas de qualificao. +Prost comeou por ser o mais rpido mas resolveu recolher s boxes, aguardando por um final de sesso com menos gua. +Pouco depois, era a vez de Senna bater o seu tempo. +Embora a pista de Donington tenha uma drenagem melhor que a de Interlagos, acho que correr aqui mais perigoso. +Com a entrada do Tratado de Maastricht na Assembleia da Repblica prevista j para a prxima semana-- depois de o Conselho de Ministros ter decidido ontem o envio de uma proposta de resoluo para ratificao parlamentar--, o debate sobre o processo e o timing adequado sua aprovao passou imediatamente ordem do dia. +Muitas questes subsistem ainda, nomeadamente sobre a necessidade de proceder a uma reviso constitucional que compatibilize o texto da Constituio com o texto do Tratado. +Hoje mesmo, o Parlamento ver-se- confrontado com a proposta de resoluo que o CDS vai apresentar, visando dot-lo de poderes de reviso constitucional de modo a permitir o recurso ao referendo que os centristas defendem para ratificar as propostas de Maastricht. +Quando no avio se acende a luz verde e comeam a encaminhar-se para a porta e a sair ao ritmo de um por cada dois segundos, as batidas do corao j normalizaram. +J nem ouvem a voz de j do instrutor-largador. +Sentem a palmada que ele lhes d no ombro e l vo. +parecem bonecos articulados. +Por vezes, muito raramente, no se ganha para o susto. +Como foi no quarto salto, em que um deles passou a velocidade meterica, com o pra-quedas dorsal em vela romana [ m abertura em que a calote do pra-quedas se mantm na forma de facho, sem superfcie de sustentao ]. +O jovem instruendo tinha aproveitado bem os ensinamentos e abriu o pra-quedas de reserva. +Respirou-se fundo, no ar e em terra. +J no cho recriminam-se entre si, por causa de pequenos descuidos. +No fui eu! +A Ligier-Renault, a Lotus-Mugen-Honda e a Sauber-Mercedes sucedero Jordan nos referidos ensaios. +A Ferrari chegar amanh ao circuito de Barcelona, onde apresentar no seu novo modelo uma das inovaes mais interessantes para a nova poca: a fixao das suspenses directamente incorporadas no chassis. +Paralelamente aos testes de reabastecimento, o McLaren-Peugeot de Mika Hakkinen e o Sauber-Mercedes de Karl Wendlinger foram os mais rpidos em pista, cronometrados na suas voltas mais rpidas em 1m19,32s, enquanto o Lotus-Mugen-Honda de Pedro Lamy foi o mais lento, gastando 1m27,52s, muito aqum do italiano Alessandro Zanardi, piloto-ensaiador da equipa, que gastou apenas 1m22,95s. +Frentzen (Sauber-Mercedes), com 1m20,02s, Irvine (Jordan-Hart), com 1m20,14s, Panis e Bernard (Ligier-Renault), respectivamente, com 1m21,64s e 1m22,24s, seguiram-se dupla mais rpida, no circuito de Montmel. +A maioria das queixas, verdade seja dita, no tinham nada de transcendente ou desconhecido. +Repetiam que as missas so uma chatice e que as homilias oscilam entre o horror e a banalidade. +Algumas pessoas atribuam misria litrgica a suposta descida na prtica religiosa. +Concluso porventura demasiado linear. +Insistiam em que os jovens dos 18 aos 25 anos j no aturavam o que os pais tinham aguentado. +O que, sendo talvez verdade, esquece que tambm h jovens para tudo. +O irrequieto Jos Magalhes resolveu brindar deputados, jornalistas e amigos com uma prenda pirata. +Uma disquete com a traduo portuguesa do Tratado de Maastricht com os conhecidos e altamente verberados erros oficiais, a que juntou uma bem humorada etiqueta: Disquete sem vrus (utilizao de software preservativo antieuropeu no aconselhada). +Uso e difuso livres. +Pirateada e distribuda gratuitamente no Natal de 1992 por Jos Magalhes a todo e qualquer interessado, de boa ou m f. +Consta que enviou uma a Manuel Monteiro e outra a Paulo Portas. +Uma pirataria completa. +No, no mais uma caso de vida ou telenovela mexicana de televiso, ou uma histria para adormecer. +Esta bem real e um exemplo de que pode acontecer a qualquer me que, ao tirar uma ecografia, descobre que no um, mas trs filhos que vai dar luz. +Uma alegria para alguns e um choque para outros. +O choque foi na ecografia, vo fazer j 18 anos, e s suspeitvamos de um. +Mas reagi muito bem e fiquei a saber que engravidei duas vezes. +Esta me explica que, em casos como este, os cuidados so todos multiplicados por mil. +No caso para menos, pois so gmeos, mas neste caso, os chamados gmeos falsos, dizigticos na linguagem tcnica, por serem gerados em vulos diferentes. +A Ins e o Joo so quase inseparveis, embora, quando pequenos, ela assumisse a funo de chefe nas brincadeiras e tivesse um instinto maternal em relao ao irmo. +So muito diferentes, ela pela sua independncia e competncia e ele pela sua preguia e boa disposio. +Sempre na mesma turma escolar, separaram-se no 10 ano. +E foi quando se notou que era a irm o grande apoio nos estudos, conta a me. +A Ins quer ser arquitecta, enquanto que o Joo j mostra tendncia para a rea da sade. +A soluo definitiva de situaes como as da central da Av. Casal Ribeiro, do Campo das Ceboulas e de numerosas outras centrais dedicadas aos transportes inter-regionais e inter-urbanos est porm dependente de uma obra de grande envergadura, ainda sem localizao completamente decidida e cujo incio no dever ocorrer antes dos prximos dois ou trs anos. +Uma vez que esta central esteja em funcionamento, passar a receber tambm as carreiras que em Janeiro sairo do Campo Pequeno para Sete Rios. +O espao a libertar nessa praa poder ento ser consagrado aos carros de turismo que agora ocupam o topo do Parque Eduardo VII. +Tambm uma residente h 30 anos nos Moinhos da Funcheira, Isabel Martins da Silva, considera excessivo o que pagou pela ligao de esgotos, 181 contos, e da gua, 81. +Para esta moradora, a autarquia ainda no fez as obras necessrias porque considera aquela urbanizao como o bairro dos ricos, como afirma ter sido dito pelo vereador do urbanismo, Miguel Vieira, da CDU. +Vrias solues tm sido propostas para contornar este obstculo. +Uma delas consiste em supor a coexistncia de Universos paralelos. +mas existe num outro, porque a nunca assassinou a sua av. + Uma outra alternativa consiste em supor que este tipo de situaes incoerentes nunca se poder verificar, porque a prpria Natureza se encarregar de o impedir. +Como? +Talvez, autorizando apenas as viagens ao passado que no dem origem temida morte da avozinha. +Ou talvez, simplesmente, proibindo qualquer tentativa de utilizao dos buracos de minhoca para construir mquinas do tempo. +Ou ainda, desencadeando a destruio imediata das mquinas que alguma vez vierem a ser construdas. +Depois de um comeo de campeonato decepcionante, a equipa de basquetebol do FC Porto confirmou ontem a sua subida de forma, vencendo o lder do campeonato por 85-84, em Ovar, com um cesto de trs pontos conseguido por Jlio Matos a trs segundos do fim do jogo. +Com este resultado, o FC Porto isolou-se no segundo lugar do Nacional, enquanto o Benfica igualou a Ovarense no comando. +O Dalkon Shield representa um episdio horrvel na histria do controlo da natalidade, mas hoje claro que os problemas se deviam a esse DIU em particular e no afectam todos os dispositivos do gnero, diz Jade Singer, responsvel do Centro de Sade de Santa Mnica. +De facto, necessrio uma certa dose de f para considerar o uso do DIU, admite Debra Bronstein, vice-presidente responsvel pelo marketing na GynoPharma. +E muitas associaes de consumidores, incluindo a Associao Internacional para a Educao das Vtimas do Dalkon Shield, continuam a fazer campanha contra o dispositivo, afirmando que no existe suficiente informao sobre os seus riscos. +A multinacional farmacutica desmentiu as notcias, acusando o jornal de afirmaes incorrectas e sublinhando ter toda a confiana na segurana de emprego e na eficcia do sedativo, desde que ele seja utilizado de acordo com as prescries referidas na literatura que o acompanha. +Comercializado sob as designaes de Midazolam (injectvel) ou Dormicum (ampolas) e venda em Portugal, o Versed um sedativo utilizado em pequenas intervenes cirrgicas, nomeadamente para anestesias locais. +Segundo a Roche, o medicamento -- utilizado nos ltimos cinco anos sem problemas por mais de 15 milhes de americanos -- foi clinicamente testado nos Estados Unidos e na Europa, no tendo sido registado qualquer morte, cujas causas pudessem ser atribuidas administrao do ' Versed '. +Os casos referidos pelo New York Times como sendo imputveis ao medicamento foram considerados pela Roche como mortes sem relao causal com a aplicao do medicamento, tratando-se de doentes cujo estado de sade j era grave e que apresentavam problemas cardiovasculares e ferimentos muito graves. +Os representantes da CNN em Bagdad comunicaram a composio do comboio e a cor dos automveis sua sede nos Estados Unidos. +Estes pormenores seriam fornecidos s autoridades americanas e, deste modo, talvez o risco de um ataque areo aliado sobre o comboio fosse reduzido. +H uma dezena de dias que a estrada entre a fronteira iraquiano-jordana e Bagdad alvo de bombardeamentos quase dirios por parte de aviao aliada. +Encontra-se cortada em diversos pontos e preciso por vezes tomar estradas secundrias, tambm alvo dos raides, como constatou este enviado ao efectuar o trajecto no dia 30 de Janeiro. +Para o bem ou para o mal, at ao momento, quem foi, na sua opinio, a figura portuguesa do ano de 1995. +E porqu? +Foi um blgaro: o Iordanov. +Porque marcou dois golos magnficos ao Martimo e, assim, o Sporting ganhou a Taa de Portugal. +J no vamos o padeiro h treze anos. +Por que razo to grave este facto? +Porque -- consideram os relatores, interpretando as diversas contribuies ao congresso -- a natureza difcil dos problemas, a escala de tempo longa da sua resoluo, a sua raiz cultural e o facto que o objecto das polticas no outro seno a mudana de comportamentos das comunidades educativa e cientfica, impem uma implicao orgnica dessas comunidades nas polticas que lhes dizem respeito. +Isto, acrescentam, sob pena de um estpido desperdcio de recursos, de energias e de motivao. +As polticas anti-participativas que os relatores consideram existir nesta rea so por eles julgadas particularmente inadequadas num perodo de expanso e de sustentado auxlio comunitrio como o actual, mas elas so, para mais, particularmente vivas no actual momento de crescimento do sistema, devido ao mais vivo confronto entre novos e velhos elementos. +A primeira dificuldade que se depara s empresas que conseguem concretizar um processo de investimento em Moambique o de recrutamento de quadros para permanncias prolongadas no pas. +Para alm de problemas salariais, mais ou menos complicados, de alojamentos difceis de encontrar e caros, a questo mais sensvel acaba por ser de ordem familiar, principalmente as relacionadas com filhos em idade escolar e com a respectiva educao. +Se no Maputo existem escolas particulares e pblicas suficientemente credveis para suprir as necessidades, nas restantes concentraes urbanas isto j no acontece. +No segundo centro populacional de Moambique, a cidade da Beira, os portugueses residentes conseguiram, por iniciativa prpria, criar uma escola que lecciona o ensino primrio e que para o ano vai iniciar-se como escola preparatria. +A organizao do 17 Festival Nacional de Gastronomia no pensa para j na sua internacionalizao, prevendo-se apenas que anualmente seja um pas da Europa comunitria a ser convidado, bem como um PALOP. +No primeiro dos casos est Espanha, que poder estar representada j na prxima edio. +As hostilidades abrem hoje, por tradio, com os paladares promovidos pela Regio de Turismo do Ribatejo, que entregou a responsabilidade da refeio a um restaurante do Cartaxo. +Tal como em todos os restantes dias, o almoo est marcado para ter incio s 13h. +Esta poca, o Sporting ainda no tinha conseguido marcar mais de dois golos nos jogos em Alvalade. +F-lo ontem, batendo um Braga pouco atrevido e superando mais um fantasma da equipa. +Mas, na bancada, j poucos acreditam no ttulo. +A transaco de 117.203 aces nominativas e 34.376 ao portador da Ocidental Holding SGPS constitui ontem uma das notas dominantes do mercado accionista nacional. +Movimentados na Bolsa de Valores de Lisboa (BVL), os lotes foram intermediados pela Comercial Dealer. +No princpio da nossa relao electrnica, acordava a meio da noite e ficava deitado a pensar se teria correio de Bill. +Ele parece escrever as suas mensagens noite, dormir (talvez) e mand-las na manh seguinte, escreve John Seabrook, explicando que a primeira pergunta por E-mail foi sobre o prprio meio de comunicao. +Sou a nica pessoa que l o meu E-mail, por isso ningum se pode envergonhar ou pensar que a mensagem vai circular por a ... +E-mail no uma boa forma para nos zangarmos, uma vez que no se pode interagir. +Seabrook notou que as mensagens de Bill no tinham princpio ou fim, coisas como caro, seu ou atentamente. +Bill nunca se lhe dirigiu pelo nome e despedia-se com um enigmtico &, que em linguagem de correio electrnico significa responda. +Para os empregados da Microsoft comum encontrarem Bill Gates electronicamente antes de o conhecerem pessoalmente e, imagem do chefe -- o mais esperto de todos --, passam a vida a descreverem-se uns aos outros como espertos e superespertos. + um conceito vago. +H uma certa vivacidade, uma capacidade para absorver factos novos. +Para perceber uma nova situao e, depois de algum lha explicar, dizer imediatamente: ' Que tal assim? ' . +Fazer uma pergunta pertinente. +Absorver em tempo real. +Capacidade para recordar. +Relacionar coisas que ao princpio no parecem ligadas. +O rbitro Pinto Correia esteve bem durante toda a primeira parte e durante quase toda a segunda. +No entanto, acabou por perdoar um segundo carto amarelo a Rui Neves, condescendendo igualmente com Dimas. +O referido relatrio explica que a substituio dos pequenos ecrs pelos multiplexes -- at ao final de 1998 est previsto o aparecimento de mais trs (Loures Shopping, Norte Shopping e Vasco da Gama) -- dever aumentar o nmero de espectadores: de 12 milhes em 1996 para 18 milhes!) em 2000. +Em termos de distribuio, os filmes norte-americanos detm praticamente 95 por cento da quota das bilheteiras portuguesas, a mais elevada da Unio Europeia. +Segundo o relatrio, as principais distribuidoras nacionais ou so subsidirias norte-americanas ou empresas portuguesas que distribuem os filmes dos grandes estdios norte-americanos. +Dois exemplos: A Castello-Lopes tem acordos com a Fox e a Miramax, a Lusomundo com a Disney e a UIP e adquire filmes da BMG. +Momento -- Andou boa parte do ano a perder tempo, tendo em conta apenas os seus interesses, a actuar no meio-campo. +Segundo observadores citados pelo Times, a China encontra-se numa posio muito mais fraca do que estava em 1982-84, perodo em que decorreram as negociaes que levaram ao acordo para a transferncia de soberania. +Nessa altura, recordam, Deng Xiaoping estava no auge da liderana do processo de reforma, era um septuagenrio vigoroso e o desenvolvimento econmico parecia imparvel. +Holanda: Bons jogadores de futebol . +Falam ingls muito bem, no parecem ter uma grande cultura prpria. +s vezes, um pouco parvos, geralmente descontrados. +Itlia: Bons a falar e a fazer negcio . +Pas bastante catico, mas muito industrioso. +Muito convencidos. +Um dos povos mais agradveis da Europa, amistosos. +Mas muita pobreza. +Fisicamente, o povo mais belo da CE. +-- Mas o senhor to cuidadoso com a sua mota que se arrisca a ser preso s para no a deixar na rua? +-- Mas normalmente assim! +As outras coisas no so to ... +O que os hngaros temem uma onda de nacionalismo nos pases vizinhos que perigue as escolas e instituies culturais de hngaras. +Isto poder levar muitos hngaros a desses pases a decidir que a emigrao para a Hungria a melhor opo. +Para resolver os complexos problemas dos hngaros que vivem nos pases vizinhos, o Governo criou um gabinete especial chefiado por Geza Entz. +Os Orlando Magic foram ao The Omni, em Atlanta, perder pela primeira vez em dez jogos seguidos da edio deste ano da NBA, a Liga Norte-Americana de Basquetebol profissional. +Os Atlanta Hawks venceram por 107-105 um jogo muito emotivo, que s ficou decidido a 32s do fim. +Foi quando John Konkac marcou os dois lances livres que desempataram o jogo, e de seguida impediu que os Magic chegassem ao cesto, obrigando-os a um turnover. +Por outro lado, no caso do haxixe os riscos para a sade so comparveis aos de produtos como o lcool, tabaco, caf e certos medicamentos. +Mais um argumento: o consumo do haxixe no provoca dependncia, ao contrrio do lcool ou da nicotina. +H alis estudos americanos que provam que o haxixe um bom relaxante, logo, se diminui o stress diminui um dos factores de risco do enfarte miocrdio. +A legalizao das drogas duras mais difcil, pois tem que ser feita escala internacional. +e no precisariam de injectar-se nos cantos do Casal Ventoso. +Isto diminuiria a criminalidade, haveria menos mortes por overdose e de outras doenas, como a sida. +Domingos -- Afinal no estava de rastos, como se quis fazer querer. +Entrou confiante, integrou-se bem nas movimentaes e mostrou que, com ele em campo, bem mais fcil rentabilizar o jogo ofensivo. +Fez trs remates, todos eles perigosos, e marcou um golo que s est ao alcance de um verdadeiro ponta-de-lana. +No tinham razo de ser as desconfianas de equipa tcnica. +Fernando Couto -- Esteve imperial, sempre muito certo a comandar a defesa. +H alturas em que parece ter um man que atrai a bola na sua direco. + Os futuros chips de 256 megabits podero ser usados em aplicaes que exigem grandes quantidades de memria, como o processamento de imagens em supercomputadores e noutras tecnologias de ponta. +A aliana entre a Hitachi e a Texas -- que uma resposta associao da IBM, Siemens e Toshiba anunciada em Julho passado nesta mesma rea -- poder ser ainda alargada a outras firmas, como a NEC e a American Telephone and Telegraph. +Alberto Ralha nunca foi alvo de grandes crticas pela simples razo de que raramente algum se lembra da sua existncia. +Beneficiando do facto de negociar com uma classe tradicionalmente pouco reivindicativa, a dos docentes do ensino superior, Alberto Ralha designado por os sindicalistas como boa pessoa, mas mau conhecedor das questes do ensino superior. +No fora o dinamismo do seu director-geral, Pedro Lince, e o mandato deste secretrio de Estado ficaria irremediavelmente marcado pela inoperncia. +enquanto as universidades pblicas ameaam fechar as portas por falta de dinheiro para pagar a docentes e no docentes, cada vez maior o nmero de estabelecimentos de ensino privados de qualidade duvidosa a ministrar somente cursos que outros recursos no exigem seno a caneta e o papel. +Esto suspensas as buscas do corpo do pescador que, tera-feira, foi engolido por uma onda, na zona da Boca do Inferno, no Guincho. +O mar continua mau, bate muito ali, e as guas esto barrentas, o que torna impossvel a aco dos mergulhadores, explicou o capito do Porto de Cascais, Fernando Tavares de Almeida. +Contrariamente ao que esperavam Lus Rodrigues e os seus correligionrios, o caso, porm, ficou adormecido no ministrio das Finanas. +A doutora Manuela Ferereira Leite [ que sucedeu a Rui Carp na secretaria de Estado do Oramento ] disse-nos que quando assumiu funes encontrou o processo meio espalhado por aqueles buracos, confirmou ontem ao PBLICO o ento vice-presidente da concelhia local do PSD, Valdemar Saleiro. +Uma das coisas que nos disseram que l vinha que havia na Cmara despesas no justificadas no valor de 194 mil contos, refere um dos outros elementos do PSD local que ento acompanhou o caso. +Valdemar Saleiro -- que tinha tambm responsabilidades partidrias ao nvel do distrito de Beja -- diz que no se recorda de ter ouvido falar neste nmero, mas confirma que a secretria de Estado o informou de que o relatrio apontava coisas que tinham de ser esclarecidas. + A sesso de ontem do Mercado Monetrio Interbancrio apresentou-se logo na sua abertura bastante pressionado pela procura de fundos, o que motivou uma subida das taxas de juro, que se foi acentuando medida que se avanava na sesso. +Com a finalidade de estabilizar o mercado, o Banco de Portugal anunciou a meio da manh uma interveno ocasional de cedncia de fundos at ao montante de 150 milhes de contos, mediante a colocao de BT/TIM a um dia e taxa de 11,75 por cento. +Apesar da presso evidenciada no mercado, apenas foram colocados 53.677 milhares de contos. +O grupo A. Silva & Silva registou, no ano passado, resultados lquidos consolidados de 13,5 mil contos. +Os resultados foram afectados pela realizao de provises extraordinrias na sua participada Assiconstri -- entretanto vendida Somague -- no valor de 330 mil contos, por forma a no transferir para o seu comprador riscos provenientes de situaes pendentes de reclamaes a clientes, eventualmente geradoras de diminuies patrimoniais futuras, refere um comunicado da empresa ontem divulgado. +Em termos individuais, a A. Silva & Silva registou lucros de 444,6 mil contos. +O volume de facturao ascendeu a 42 milhes de contos, os resultados operacionais consolidados foram de 1,1 milhes de contos (contra 652,4 mil contos registados em 1995), enquanto os resultados correntes totalizaram 342,4 mil contos (mais 159,2 por cento do que no ano anterior). +So pequenas partculas metlicas e foram descobertas incrustadas nos ossos dos calcanhares do piloto Jorge Albuquerque. +A sua importncia? + que ningum consegue explicar como que elas podero ter ido a parar a no ser devido acelerao provocada por uma exploso. +E possvel que o material que as compe seja aquele de que era feito o recipiente que conteve a bomba. +Trata-se de uma das provas fundamentais apurada pela V CEIAC, a par da descoberta dos vestgios de explosivos em peas do avio. +Clinton apressou-se a dizer que tais contactos no deviam ter acontecido e a sua porta-voz, Dee Dee Myers, indicou que documentos sobre esses encontros foram compilados e seriam entregues a Fiske, respeitando a intimao feita por este. +O procurador especial chamou a depor seis funcionrios da Casa Branca e quatro elementos do Departamento do Tesouro, para esclarecerem os contactos mantidos. +Entre os primeiros, conta-se o advogado Bernard Nussbaum, amigo de Clinton e que se demitiu na semana passada, em relao com o caso. +A opo do Alentejo vai para o chamado Turismo Verde, em zonas de inequvoca qualidade ambiental. +Um turismo que promova a descoberta da diferena e no a massificao, diz Andrade Santos, presidente da RTE. +Alm da costa martima, a melhor preservada do pas, o Alentejo oferece turismo rural e agro-turismo, turismo cinegtico e o aproveitamento de albufeiras, recorrendo sempre boa qualidade do seu patrimnio natural. +E a riqueza do seu patrimnio construdo, a cultura musical regional, o artesanato e a gastronomia tradicional completam, em linhas gerais, o quadro da regio. +Ultimamente, a implantao de campos de golfe tem surgido como um complemento turstico no desprezvel, ainda que assumindo, por vezes, discutveis propores (ver caixa). +O romance da princesa Ana -- segunda descendente e nica filha da rainha -- com o comandante Tim Lawrence, 37 anos, j fizera as delcias da imprensa popular britnica, nomeadamente na altura em que o casal apariceu em pblico, por diversas vezes, aps o divrcio de Ana. +A data e local do casamento, que dever realizar-se numa cerimnia privada, no foram divulgadas pelo Palcio de Buckingham. +Face ao impacte dos planos do construtor, a comisso de trabalhadores da Renault Portuguesa foi ontem ao Ministrio da Economia lembrar ao Governo o compromisso francs de criar 180 novos postos de trabalho em Cacia e investir nesta unidade 12 milhes de contos. +A resposta do Governo que o assunto tem estado na agenda dos contactos com o grupo francs, uma posio que para os trabalhadores no representa qualquer garantia. +Os representantes dos trabalhadores esperam pelo comit de grupo europeu extraordinrio para uma deciso quanto adeso a uma eventual greve dos efectivos do construtor. + Contudo, o porta-voz da comisso de trabalhadores, citado pela Lusa, defendeu que a reduo de postos de trabalho na Renault Portuguesa mais significativa, ao ter passado de 3500 trabalhadores em 1992 para 1200 em 1997. +Em 35 anos, os danos causados ultrapassam dois mil milhes de pesetas (2400 milhes de contos), diz o relatrio. +Acrescenta que esta estimativa se refere unicamente aos danos directos dos sinistros e no inclui os custos da luta contra os incndios -- verbas avultadas, como pode concluir-se pelas destinadas ao ano em curso, que atingem os 130 mil milhes de pesetas (156 milhes de contos). + Outro aspecto preocupante o da frequncia crescente de anos negros, em que se registaram recordes de zonas ardidas. + De 1978 at agora, Houve quatro anos em que se registou a perda de reas superiores a 400 mil hectares. +O ano passado esse valor foi de 432 mil hectares, estando o recorde no ano de 1985, com 484 mil hectares. + A Assembleia Parlamentar da Francofonia (APF, ex-AIPLF), reunida em Abidjan, na Costa do Marfim, condenou ontem a insurreio desencadeada h um ms por certos elementos das Foras Armadas da Guin-Bissau e exortou as partes em conflito ao dilogo no respeito pela legalidade constitucional. +Numa resoluo adoptada em sesso plenria, a APF reafirma a legitimidade do actual governo, eleito em eleies livres e democrticas e condena sem reservas a rebelio iniciada em 7 de Junho de 1998. +A Assembleia exprime o seu apoio total ao governo do Presidente Nino Vieira, e lana um apelo premente a uma cessao imediata das hostilidades. +Sendo o primeiro de uma nova gerao de jogos a trs dimenses para consolas de 16 bits que a Nintendo lanou no mercado de vdeojogos, Starwing surpreendente pela qualidade dos grficos que apresenta, pelo som, perfeitamente adequado ao ambiente grfico, mas sobretudo pela extraordinria velocidade a que se desenrola. +O jogador -- que desempenha o papel de Fox McCloud -- tem de completar um total de 18 misses, sobrevoando planetas ou enfrentando os inimigos no espao, onde as dificuldades so acrescidas, j que, para alm dos opositores, tem de desviar-se de autnticas chuvas de asterides e outros objectos que vagueiam sem rumo. +Tem tambm de zelar pela segurana da sua equipa de pilotos, ajudando-os sempre que esto em dificuldade. +Aps algum treino, os comandos so fceis de manejar, permitindo ao jogador executar piruetas ou utilizar o propulsor para acelerar momentaneamente a sua nave. +H trs formas de visionamento do cenrio, escolha do jogador. + Nas cenas de espao a viso de ' cockpit ', com o recurso a uma mira, de grande utilidade. +O programa de trabalho e o oramento para os prximos trs anos para o Observatrio Europeu das Drogas e das Toxicodependncias, com sede em Lisboa, foram ontem discutidos numa reunio em que participaram representantes de todos pases da Unio Europeia. + O PCP, esse, est sempre bem e com pouco trabalho pela frente. +O que era preciso fazer, est feito: derrotar uma iniciativa do Governo. +Distribudos os trabalhos, falta perceber como se chegou aqui. +O que tambm no difcil. +Tudo nasce num triplo erro governamental. +Reduzir a procura do consumo de droga a prioridade do rgo Internacional de Controlo dos Estupefacientes da ONU. +Um relatrio divulgado hoje, que d conta dos receios causados por uma cultura propcia ao consumo e que condena as vozes mais tolerantes sobre o uso de narcticos, revela uma descida do nmero de consumidores de herona na Europa e uma subida do cultivo de cannabis e do consumo ocasional de estimulantes e alucinogneos. +A herona cada vez mais uma droga fumada e menos injectada. +A Norquifar refere ter obtido do presidente do Infarmed, Aranda da Silva, um compromisso no sentido de permitir uma distenso mais razovel, nunca inferior a trs anos, do tempo para a aplicao do decreto e queixa-se de ter sido surpreendida, pouco tempo depois, com as notificaes. +O cumprimento do decreto [135/95] nos prazos em que agora o Infarmed veio exigir quase impossvel porque o novo licenciamento pede documentos que so difceis de obter num curto espao de tempo -- como sejam a obteno de alvars camarrios ou pareceres de segurana do Servio Nacional de Bombeiros, diz Srgio Figueira, da Norquifar. +Quando os prazos se esgotarem, explicou ainda, os armazns correm o risco de ser fechados por qualquer aco de inspeco daquele instituto. +Alguns prazos esto, segundo Srgio Figueira, a poucos dias de chegar ao limite. +A Norquifar, que j se tinha queixado de se estarem a criar empregos por decreto, entende que a contratao de um licenciado a tempo inteiro para os armazns no s demasiado onerosa para as pequenas e mdias empresas que representa, como injustificvel, pois um armazm grossista no vende directamente ao pblico. +Afirmou um dia ao PBLICO que o seu inimigo principal era o poder poltico. +H um ano, achava muito divertidas as notcias que a davam como possvel candidata do CDS-PP s legislativas. + Nunca pensei entrar na poltica!, jurava Manuela Moura Guedes. +ao fim de 16 anos, deixava a RTP para ir apresentar o Telejornal da TVI. +Foi sol de pouca dura.. +Em Junho, afastada dos noticirios h meses, era outra vez notcia. +Rumores de que estava a caminho da SIC -- apesar das resistncias de pesos pesados da redaco do canal de Carnaxide --, enquanto outros a davam com um p nas listas do renovado PP. +Est tambm prevista a presena dos presidentes das entidades estatais mais ligadas aplicao do programa: Instituto Nacional da Habitao e IGAPHE Instituto de Gesto e Alienao do Patrimnio Habitacional do Estado. +A indignao dos dirigentes do Sindicato dos Txteis tem como principal alvo a Cmara Municipal, que alegadamente ter colocado entraves que fizeram baixar o valor do imvel. +Em causa estar, nomeadamente, a impossibilidade de alterar a fachada do edifcio de quatro andares, cada um deles com cerca de seis metros de p-direito. +O prdio foi agora vendido empresa Construes Progresso no mbito de negociaes particulares. +Estas foram iniciadas h meses, depois de se considerarem esgotadas as hipteses de venda por arrematao judicial, dado os valores oferecidos -- num mximo de 186 mil contos -- estarem muito aqum da avaliao. +Foi melhor do que o que eu esperava, comentava uma mulher no elevador, acerca do Show de Moda Primavera / Vero que anteontem noite teve lugar no Shopping Center Cidade do Porto, no Bom Sucesso, e que se dever vir a repetir oportunamente. +Aconteceu numa passarelle montada sobre a pista de gelo e rodeada por cadeias dirigidas aos diversos convidados de cada uma das lojas que acederam em participar. +Os sem convite permaneceram atrs das cadeiras e nas varandas dos trs pisos do shopping. +Curioso foi o facto de o show se ter dividido em duas partes e das noivas surgirem logo aps o intervalo e no no final da passagem, como hbito nestas coisas. +Antes, ao fim da tarde, os manequins e crianas passaram modelos da Mango, Union Blue, Authentik, Quiosque, Lanidor, Jacadi, Inquietao, Action Sport, Nexus, Petit Patapon, Cenoura, Boxer Shorts, Nastra, Bambini, Tin Tin e Miss Selfridge. + noite, numa prespectiva mais clssica, desfilaram coleces da Pronuptia, Gianonne, Lace, Mac Moda, Pinto's, Cortefiel, Alain Manoukian, Rondssimo, Globe, Springfield, MyGod, Vitalis, rbrica, Manel Boutiques, Veneza 5, Nogaret, Acapicua, Sugar, Decnio e Paulina Figueiredo. +Um rol de propostas, prt--porter. +R. -- A ideia agrada-me e j fiz, inclusive, algumas experincias. +De qualquer modo, aquilo que se passa, em termos de electrificar a sanfona ou os outros instrumentos, que os espectculos do Realejo so na sua maior parte acsticos. +Essas experincias de electrificao no tm resultado, at agora, muito bem, embora no disco haja uma faixa que aponta um bocado nesse sentido, a cantiga de Santa Maria, com um movimento meio arrockalhado. +Pode ser um ponto de partida ... + P. -- Mas por enquanto o Realejo continua a ser um grupo de e para interiores, no duplo sentido da palavra? +Considerado culpado mas absolvido pela justia militar italiana, o ex-nazi Erich Priebke foi de de novo preso mas isso no lava o sentimento que cobriu uma maioria da Itlia: a vergonha. +Pelo veredicto, pela maneira como o processo foi conduzido. +Para os juzes, os crimes de Priebke prescreveram. +Como disse o Presidente Scalfaro, nunca prescrevero na memria dos italianos. +A Direco do Torreense, cuja a equipa de futebol milita na 2 Diviso de Honra, demitiu-se em bloco na Assembleia Geral realizada na noite da passada segunda-feira. +Razo: a Cmara Municipal de Torres Vedras (CMTV) no tem ajudado o clube financeiramente. +Chegmos a um ponto em que deixaram de existir condies financeiras que garantam uma gesto responsvel do clube, garantem os dirigentes. +Antnio Jos dos Santos, o presidente demissionrio, prometeu continuar a assegurar a gesto corrente do clube e admitiu reconsiderar a sua posio e ficar at final do mandato, mas s depois de saber com quanto que Cmara pode auxiliar o clube. +Jos Augusto Carvalho, presidente da CMTV, considera profundamente injustas as acusaes lanadas sobre a autarquia. +A cmara s se comprometeu a contribuir com igual montante ao total da soma dos restantes scios-empresas e cumpriu, disse Jos Augusto Carvalho. +A Federao Portuguesa de Andebol vai analisar amanh, em reunio do executivo, a no realizao, no passado sbado, do jogo FC Porto- Ginsio do Sul, referente segunda jornada do Nacional. +O executivo federativo est j a fazer um levantamento de toda a documentao, que dever ser complementada com relatrio da equipa de arbitragem nomeada para dirigir a partida. +Segundo uma fonte da FPA, o jogo foi marcado para o Pavilho das Antas e o FC Porto recebeu a notificao atravs de comunicado, logo pode-se concluir que o recinto j no se encontrava interdito. +A resoluo do problema pode agora passar por um acordo entre os clubes. +Caso isso no venha a verificar-se, a Federao vai instaurar um processo onde o FC Porto ter de apresentar as razes que levaram a equipa a comparecer em Aveiro e no nas Antas. +um pacato cidado, livre de toda a suspeita, um homem simptico e prestvel, guarda em casa 24 cadveres mutilados. +Descoberto por acaso, revela-se algum que encara os crimes como algo natural. +Para a psiquiatra que o observa, um ser intrigante; para a polcia, uma encarnao do demnio. +Jeff Goldblum e Alan Bates em confronto. +J perdemos algumas cartas altas, ainda assim, temos algumas figurinhas, desabafou ao PBLICO um dirigente nacional do PP para justificar o silncio dos notveis. +Fernandes Thomaz, Lus Queir e Celeste Cardona so as personalidades que restam, enumerou, para depois concluir que Monteiro no pode esquecer isso, caso decida recandidatar-se. + A moo da distrital do Porto, A casa comum da direita, tambm procura congregar um maior nmero de apoiantes. +Slvio Cervan conseguiu a adeso do presidente da distrital de Aveiro, S Correia, prximo de Paulo Portas, e da concelhia de Vila Nova de Famalico, uma estrutura do distrito de Braga, de Antnio Pedras. +A aplicao aos avies comerciais de passageiros dos sistemas electrnicos de defesa antimssil, em uso na aviao militar, est a ser equacionada pelo governo norte-americano. +Ontem, na Casa Branca, reuniu-se pela primeira vez uma comisso constituda por 21 especialistas em sistemas de defesa e contramedidas electrnicas. +Segundo Al Gore, vice-presidente dos Estados Unidos, esta comisso deve apresentar um relatrio de concluses na prxima tera-feira. +Os dez msicos que estaro em palco, incluindo Bryan Adams, fizeram um nico pedido -- querem comida vegetariana nos camarins. +Os promotores do concerto recomendam aos espectadores: levem guarda-chuva no v S. Pedro pregar uma partida. + Reconhecendo possuir algumas saudades do Parlamento, a autarca afirmou que preside a um concelho apaixonante, apesar das contradies, mas lamentou que este tenha estado esquecido pelo poder central e desmobilizado pelo poder local. +Da a meter uma cunha aos antigos colegas foi um passo. +Muitos aspectos da legislao das autarquias, sustentou Edite Estrela, so inibidores da aco e da criatividade e verdadeiros obstculos a vencer pelos autarcas. + prefervel haver leis mais flexveis, para serem cumpridas, do que leis muito restritivas que, por vezes, no so cumprveis, disse, criticando a suspeio com que os autarcas so tratados pelo Estado. +Em troca defende que os prevaricadores sejam castigados exemplarmente. +Em Paris, a bolsa tambm registou alguns ganhos com a abertura em alta do Dow Jones, apesar de uma sesso calma que resultou num aumento de 0,56 por cento, com o CAC 40 a encerrar nos 2940,89 pontos. +Em Madrid, o ndice geral subiu 0,44 por cento. +Duas outras personalidades que se digladiam, sem dvida, pelo primeiro plano no crculo do poder so o presidente da Cmara de Moscovo, Iuri Lujkov, aliado dos banqueiros e das mafias da capital, e Anatoli Chubais, o ex-vice-primeiro-ministro recm-nomeado chefe da casa civil da Presidncia. + Gestor genial, reformista moderado e autor do programa de privatizaes do Governo, Chubais tem a seu favor o apoio dos governos ocidentais e do FMI, que tendero a ver nele o garante da continuidade da poltica das reformas, quando Ieltsin j no puder impor a sua vontade. +Mas, tendo tambm em conta a histria recente, no um dado adquirido que Ieltsin esteja acabado. + No incio deste ano, em plena catstrofe tchetchena e aps dois ataques cardacos, a popularidade do Presidente no chegava aos dez por cento. +e, no auge da campanha eleitoral, foi visto a gritar e cantar, a correr e a danar o twist e o rock'n roll. +Inauguram no Lagar do Azeite, em Oeiras, pelas 21h30, o X Salo Livre de Humor / Oeiras 97 e o VIII Festival Internacional de Humor. +Patente ao pblico at ao prximo dia 26, de Segunda a Domingo, das 14h00 s 19h00. +O Dirio da Repblica no tem o exclusivo de publicao das boas notcias legais ou regulamentares. +Prova indiscutvel disso a Circular 3/94/DEP/1 do director-geral dos Servios Prisionais que veio regulamentar o controlo e reteno da correspondncia dos reclusos, com vista a que o mesmo esteja em harmonia com o quadro constitucional de previso e tutela dos direitos fundamentais. +A partir de agora, a correspondncia s ser aberta na presena dos reclusos, para verificar do eventual envio de bens no autorizados e a mesma s ser lida em casos devidamente justificados. + O jornal popular alemo Bild anunciou ontem que o construtor automobilstico alemo Audi vai pagar 200 milhes de marcos (cerca de 20,4 milhes de contos) pela marca italiana Lamborghini. +A notcia citava especialistas da indstria automvel, mas ainda nenhuma das partes envolvidas na transaco divulgou o montante envolvido. +A Audi j tinha anunciado o seu interesse na compra da totalidade do capital da Lamborghini, cujo principal accionista o filho mais novo do ex-presidente indonsio Suharto. +Jorge Petiz, em Porsche Carrera RSR, concorrente ao Campeonato Nacional de Velocidade (CNV) de Clssicos, foi ontem o piloto mais rpido no conjunto das duas sesses de treinos para a Rampa da Arrbida, pontuvel para o Nacional e trofus monomarca e Challenge FIA de Montanha, prova que esta manh se disputa. +Petiz realizou a melhor subida em 1m44,084s, superando o seu prprio rubricou ao volante do BMW M3 do CNV (classe N2), fixado em 1m45,887s. +Na classe at 2 litros (N1), Carlos Borges (Toyota Carina E) imps-se com 1m51,484s. +Agarrou-se causa como uma nufraga, e seria compensada. + maneira do regime. +Com uma medalha por servios prestados comunidade enquanto Trmerfrau -- destas, s foram entregues duas --, uma certido numa capa vermelha com o smbolo da RDA a dourado, uma viagem em paquete para os activistas da primeira hora, onde viajou ao lado de Erich Honecker, da bailarina Great Paluka e muitos outros. +E a esttua. +A maior honra, porm, ter sido esse convite para falar em Berlim, frente a 100 mil pessoas no dia do trabalhador da construo, em 1979. +Se foi ela a autora do texto? +Qual qu, eles que prepararam tudo. +Eu s tive de ler, o que, pela maneira como o diz, ter sido um grande alvio. +Poder-se- dizer que o estilo resulta da sua profisso, fotojornalista. +Trata-se do contrrio. + Ele faz jornalismo fotogrfico, porque esta profisso a que melhor se adequa ao seu estilo, sua maneira de olhar o mundo. +H, no seu modo de ser, como que uma espcie de timidez, que o leva a fazer fotografia no intrusiva, uma das maiores dificuldades do ofcio. +No conheo fotografia sua onde os sujeitos estejam pouco vontade, furiosos por estarem a ser fotografados, envergonhados, com o sentimento de estarem a ser violados na sua privacidade. +Os que olham para a sua cmara, com mais ou menos prazer, com ou sem indiferena, no mostram hostilidade ao fotgrafo. +E os que no olham para a cmara, no parece, pelo que esto a fazer, que reagiriam contra o que os observa. +No conheo fotografia sua que revele uma intimidade secreta, algo que ningum gostaria que se soubesse ou visse. + difcil, sendo-se fotgrafo, cultivar sem concesses este respeito pelos outros. +Muito mais ainda, sendo-se fotojornalista, profisso em que o voyeurisme pode ser elevado ao estatuto de virtude, como acontece tantas vezes com as revistas e os jornais sensacionalistas, como praticam to obsessivamente muitos jornalistas nas televises contemporneas. +Nas suas fotografias, Alfredo Cunha revela uma humanidade quase inocente, uma comovente igualdade perante os outros. +No se consegue perceber que o fotgrafo ganha a sua vida custa dos outros. +so a demonstrao exacta da ternura tmida que o habita. +O jornalista no parte do princpio que tem direitos sobre os outros, nem sobre os seus sentimentos, nem sobre as suas obras. +Ele sabe o que o direito informao, mas no o reduz aos seus prprios direitos, que considera limitados pelos seus deveres. +No conheo fotografia sua na qual a dor, o desespero, a clera, a volpia ou a intimidade sejam explorados. +E, no entanto, as suas fotografias so capazes de nos revelar as pessoas, os sentimentos, as situaes. +Tanto mais que Alfredo Cunha tem simpatias humanas, culturais e polticas. +Fotografa os poderosos por profisso, os sem poder por vocao. +Visivelmente, prefere os pobres, os que trabalham, os que sofrem. +Nestas condies, muito difcil no fazer fotografia empenhada, engag, com funo e utilidade polticas. +Ele consegue-o. +Fotografa um lado da condio humana, mas f-lo sem pragmatismo, sem intuito propagandstico, sem outra inteno que no seja mostrar-nos o que ele v, como ele v, o que ele prefere. +No encontro, a Sun apresentou a sua nova estratgia The Road to Java e o projecto Java Centre Program, iniciativas que pretendem promover a adopo do Java pelas empresas, integrado numa arquitectura de network computing (baseada na rede e no conceito de thin clients). +Ao abrigo do Programa de Centros Java (Java Centre Program), a Sun vai continuar a abrir Centros Java (Java Centres), geridos pela prpria Sun, e Centros Autorizados Java (Authorized Java Centres), geridos por parceiros, que fornecero servios profissionais para instalao de projectos baseados em Java nas empresas. +Neste momento, j existem cerca de 225 centros destes em todo mundo, onde cerca de trs a cinco mil profissionais oferecem um conjunto de servios de consultoria, integrao de sistemas, formao e manuteno. +A sua unificao, agora, ao abrigo do Programa de Centros Java, uma tentativa da Sun de reorganizar os servios j existentes na rea das solues de network computing baseadas em Java -- explicou a COMPUTADORES Mark Tolliver, vice-presidente responsvel por Market Development. + Ao que o PBLICO apurou, as razes que tm levado o ncleo duro da metalrgica a opor-se entrada da Cortal / Seldex no elenco dirigente tem pouco que ver com o receio de um eventual controlo da empresa pelo concorrente, e muito mais com a inconvenincia de desvendar estratgias empresariais em preparao. +Com efeito, nos ltimos anos a F. Ramada tem tomado uma srie de medidas de reestruturao e modernizao. + o caso da diminuio substancial do quadro de pessoal, que passou de quase 1200 trabalhadores em 1987 para os actuais 600, o que custou quase meio milho de contos empresa. +Tambm a perda do mercado angolano levou a administrao da F. Ramada a preparar, atravs da recm-formada empresa de import-export Ramada Internacional, a conquista de mercados alternativos, nomeadamente os dos pases do Magrebe, da Amrica do Sul e da Espanha, esta ltima uma das grandes apostas, precisamente, da Cortal. +H ainda a inteno de avanar com um projecto imobilirio no centro de Ovar -- nos terrenos da antiga sede da empresa --, com o que a administrao conta arrecadar 2,5 milhes de contos, para um investimento calculado em um milho. +A Organizao dos Pases Exportadores de Petrleo (OPEP) vai pedir a todos os seus membros, na prxima reunio, marcada para 13 de Fevereiro, para que reduzam a produo de crude, de forma a inverter a tendncia de queda do preo do barril. +Segundo o seu presidente, o venezuelano Alirio Parra, so necessrios cortes da parte de todos os Estados, pelo que se impe a sua adeso sem excepes a esta medida. +Problemas: Distribuio e venda de droga. +Obviamente que no quero dizer que as carcias de uma me possam ser substitudas de forma mecnica. +O contexto onde as coisas se passam importante e nada disto quer dizer que os elementos psicolgicos no sejam importantes, observa Uvnas-Moberg. +Mas o que eu digo que, quando h, por exemplo, uma criana que est numa incubadora e se no pode contar com a estimulao sensorial que a me normalmente lhe concederia, conveniente proporcionar-lhe essa estimulao. +A investigadora lembrou que tambm existem drogas que tm o mesmo efeito. +Uma droga que nos deixa ligeiramente sedados, que aumenta a nossa capacidade de interaco social, o lcool em baixas doses, que tambm faz subir a oxitocina, continua Uvnas-Moberg. + H tambm drogas que propiciam a libertao de oxitocina, como certos neurolpticos, e existe tambm oxitocina em ' sprays ' nasais, que certas mulheres usam para facilitar a amamentao, quando h problemas na produo de leite. +A revista comeou a ser feita exclusivamente por Lourdes Castro e Ren Bertholo. +Os primeiros nmeros eram como que uma carta aos amigos. +Depois, iniciou-se a colaborao de todos os elementos do grupo e de muitos outros portugueses, ligados colaborao escrita, emigrados ou no, como Joo Vidal, J. M. Simes, Jos Gil, Helder Macedo, Nuno Bragana, Cristovam de Pavia, Alfredo Margarido, Antnio Areal, Jos-Augusto Frana; ou estrangeiros, como Andr Pieyre de Mandriargues, Karl Laszlo, Benjamim Peterson. + o 11, Primavera de 1963, homenageia Yves Klein). +O trabalho fazia-se no atelier -casa de Lourdes e Ren, que possuam uma mquina de impresso serigrfica onde todas as imagens originais da revista eram realizadas. +A ttulo de exemplo, citem-se originais (que os assinantes da revista recebiam devidamente numerados, assinados e datados) de todo grupo e ainda de Vieira e Arpad, Saura, Millares, Peter Saul, Corneille, Tinguely, Klein, um objecto op de Soto, postais sobre desenhos originais de Le Parc, Cruz-Diez, Alberto Greco, Aleschinsky, Telemaque, etc. +Tendo-se tornado difcil avanar com um colectivo to alargado e dispersivo, Lourdes e Ren resolveram restringir a concepo da revista a eles prprios, Christo e Voss e acordaram em que cada nmero seria da responsabilidade individual de um deles, com a colaborao dos restantes. +Assim concebem e realizam os melhores nmeros da revista (do 9 ao 12, sados entre 1962 e 1963), sob a sucessiva direco de Voss, Ren, Christo e Lourdes Castro. + e, finalmente, Lourdes, que iniciava a explorao das suas sombras, concebe uma obra essencialmente visual: mais de 50 postais organizados em pginas picotadas, onde o recorte e o contorno so decisivos, mas a imagem fotogrfica se revela tambm essencial. +O secretariado da comisso poltica distrital de Leiria do PS rene-se na prxima segunda-feira e a substituio do governador civil ser um dos temas a abordar, como afirmou ao PBLICO o seu presidente, Jos Canha. +Apesar de a escolha para governador civil de Leiria recair sobre Alberto Costa, Jos Canha informou que a comisso poltica de Leiria tem mantido contacto com o ministro nos ltimos dias, com o intuito de lhe transmitir o seu parecer. +O peso (a durao) diferente que d a cada som cria uma riqueza fascinante na iluso de timbres e de dinmicas. +A manipulao do tempo (do ritmo, da essncia do fenmeno musical) recria todas as outras dimenses do som, numa capacidade de reinventar cada arquitectura musical que fez deste recital um acontecimento nico. +Voluptuoso na suite em r menor de Louis Couperin, Leonhardt tornou o prlude non-mesur de Anglebert numa verdadeira dissertao filosfica, cujo final, no seu despojamento elptico, surgiu como um ponto de interrogao metafsico e perturbante. +Para cada trecho de Franois Couperin, Royere, Forqueray, um esprito mais comedido no prescindiu de enorme fantasia, por vezes de transbordante emoo numa atitude comunicativa e interiorizada, simples e aristocrtica, reveladora de uma fascinante personalidade de intrprete. +O resultado da sua primeira aplicao poder ser visto, em breve na Cidade Universitria, em Lisboa, local onde ir ser construda, com base neste molde de beto, uma galeria tcnica, cuja concepo permitir diversas intervenes no subsolo sem qualquer incmodo para os transeuntes. +Trata-se, segundo o projectista Martins de Oliveira, que concebeu o molde, de uma conduta visitvel, capaz de reduzir ao limite mximo todos os inconvenientes produzidos pelas permanente operaes de manuteno levadas a cabo pela Cmara Municipal, EDP, Telecom e TLP. +Com este tipo de modelo, a renovao das vrias redes, ao nvel do saneamento bsico, sero executadas sem perturbar terceiros, explicou Martins de Oliveira. +Embora as aces dos trabalhistas, enquanto oposio, tenham sido extraordinariamente cautelosas em relao Irlanda do Norte, no poder podero enfrentar um cessar-fogo renovado tendo a completa noo das consequncias de um possvel falhano. +E enquanto por um lado a administrao trabalhista no depender dos unionistas para obter o apoio parlamentar, por outro, necessitar que eles se mantenham comprometidos para assegurar o avano das negociaes de paz. +Se o IRA apelar a um cessar-fogo aps as eleies muito provvel que se d incio a um segundo processo de paz. +Mas ser que ir ser muito diferente do primeiro, e quais so as suas hipteses de sucesso? +O sismo de segunda-feira em Los Angeles no poupou ningum, nem mesmo as estrelas. +Michael Jackson fracturou a cabea e os dedos da mos, a cantora Dolly parton ficou sem nariz e os actores Oliver Hardy e Mae West perderam acabea- mas no literalmente. +Todas estas tragdias artsticas deram-se com as rplicas daquelas estrelas no museu de cera da cidade, que no escapou aos efeitos do sismo. +Sylvester Stallone foi uma das figuras que se manteve impassvel e no se desmoronou enquanto a terra tremia. +Em carne e osso como em cera, eis um homem impassvel. +O secretrio de Estado da Integrao Europeia, Vtor Martins, seguir hoje directamente de Bruxelas (onde preside ao Conselho do Mercado Interno) para Rabat (Marrocos), para manter conversaes com o ministro marroquino dos Negcios Estrangeiros sobre os resultados do Conselho de Ministros dos Estrangeiros dos Doze que se realizou a 17 de Fevereiro em Lisboa, onde foi aprovada uma proposta visando a concluso de um acordo de comrcio com Marrocos. +A par de um medicamento antiacidez, o grupo aconselha especificamente dois antibiticos de entre a Claritromicina, Metronizadol e Amoxilina, prescritos apenas durante uma semana. + que se receia que a escolha de um tratamento incorrecto e disseminado escala planetria faa aumentar a resistncia da bactria aos antibiticos. +O grupo tambm recomenda fortemente a eliminao da bactria em doentes com um tumor raro chamado linfoma de MALT (Tecido Linfide Associado Mucosa), quando ainda est numa fase de baixo grau de malignidade. +Vrios estudos tm mostrado que estes linfomas regridem depois de tratamentos dirigidos HP, o que sugere uma relao entre os dois. +A Assembleia da rea Metropolitana de Lisboa (AML) continua a debater-se com o problema da falta de instalaes, mas pelo menos j tem mesa. +A eleio decorreu ontem, num acto com algumas peripcias, mas sem grandes surpresas, j que se cumpriu o acordo previamente estabelecido entre a CDU e o PS. +Se no se der qualquer fiasco de ltima hora, provvel que o PDL mantenha uma maioria confortvel dos 299 lugares da Assembleia. +Os coreanos pensam, em geral, que a poltica no devia ser controlada pelos homens de negcios poderosos, afirma Ahn Byung Joon, professor de Cincias Polticas na Universidade Yonsei de Seul. +e uma vitria do PUN com essa amplitude parece perfeitamente plausvel. +Chung deu voz a uma poderosa corrente de insatisfao, especialmente entre os eleitores das classes mdias. +Embora a economia da Coreia do Sul tenha crescido cerca de nove por cento no ano passado, so muitos os cidados que vem com preocupao o dfice comercial do seu pas, que de 8,8 mil milhes de dlares [ cerca de 1250 milhes de contos ]. +Chung afirma que o problema haver demasiada interferncia do Governo nas finanas e poucas medidas contra as barreiras proteccionistas dos japoneses. +O pblico no deve ser desinformado por esta campanha publicitria, disse o comissrio para os Assuntos Sociais, Padraig Flynn. +os no fumadores devem ter prioridade. +Estas declaraes surgem a poucos dias de, mais uma vez, o Conselho de Ministros da Sade da Unio, discutir uma proposta que visa banir completamente a publicidade ao tabaco. +Em Portugal e na Itlia esta publicidade j no possvel, mas em vrios outros pases da Europa ainda persiste, embora com restries diversas. +As propostas abolicionistas tm encontrado a oposio de lobbys tabagistas gregos e espanhis. +Esperam-se, agora, as oportunas reportagens do golpe de 1974 sem tanques nas ruas, nem cravos, nem vivas ao MFA. +E mesmo assim de madrugada, com bola branca a avisar os mais sensveis. +Nessa altura a RTP ter atingido a perfeio. +E ns teremos esgotado a pacincia ... +Teatro e msica, pelo grupo Meia Preta, pelas 22h00, na Fortaleza, em Armao de Pra. +Em Lagoa, inaugurada a Fatacil -- Feira de Artesanato, Turismo, Agricultura, Comrcio e Indstria. +Tomislav Ivic, que foi treinador do Benfica e do FC Porto, esteve na Luz na quarta-feira a assistir ao jogo entre as suas duas antigas equipas [ 1 mo da Supertaa ]. + Penso que o Benfica tem muitos talentos, tem belssimos jogadores, mas penso tambm que o FC Porto, pelo que vi na quarta-feira, mais agressivo sobre a bola do que o Benfica. + A exposio abre com o quadro de Jlio Pomar Fernando Pessoa e inclui, entre outras, obras de Carlos Botelho, Joo Abel Manta, Joo Hogan, Bernardo Marques, Mrio Eloy e Maluda. +Inaugurao de uma exposio de escultura de Rui Vasquez. +s 19h30 no Hotel Alfa, em Lisboa. +Patente ao pblico at 10 de Maro. +Foi descoberto em 22 de Maro deste ano por astrnomos americanos, quando um rasto da sua cauda foi avistado na atmosfera de Jpiter. +Em Julho do prximo ano, muitos dos telescpios do mundo inteiro vo querer assistir ao seu fim. +Mais de 20 filmes esto em competio a partir de hoje na 46 edio do Festival de Cinema de Locarno, que se realiza at 15 de Agosto. +Da lista fazem parte filmes como Au nom du Christ, do realizador Roger Gnoan M'Bala (Costa do Marfim), Beijing Zazhong, de Zhang Yuan, considerado o primeiro filme punk-rock chins, Bhaji on the Beach, de Gurinder Chadha (Gr-Bretanha), Koraku Zaru, do japons Kenchi Iwamoto, L'crivain public, de Jean-Franois Amiguet (Frana), L'Ordre du jour, do palestiniano Michel Kheifi, La Ribelle, do italiano Aurelio Grimaldi, La Vida Conyugal, do mexicano Lus Carlos Carrera, e Marcides, do egpcio Yousry Nasrallah, entre outros. +Para o Leopardo de Ouro e o Prmio da Cidade de Locarno, o jri deste ano constitudo pelos realizadores Chantal Akerman (Blgica), Olivier Assayas (Frana), Kathryn Bigelow (Estados-Unidos), Ferid Boughedir (Tunsia), Alexei Guerman (Rssia), alm de um pintor italiano e de um produtor da televiso sua. + O festival prope ainda um panorama sobre a produo sua recente, filmes sobre filmes na seco Cinma-Cinmas, uma homenagem ao realizador italiano Valerio Zurlini e a primeira retrospectiva integral da obra de Sacha Guitry. +Para ressarcir a Cmara das despesas a suportar, bem como do direito montagem do equipamento, ir ser estabelecido um contrato de comodato de um bloco sanitrio e dois abrigos para passageiros. +Em princpio chegou pensar-se instalar um jornal electrnico, mas isso implicava a compra dum computador demasiado caro para o municpio. +Apesar de algumas informaes que do conta alguns casos em que o nome da firma aparece citado, o presidente da Cmara de Alcobaa, Miguel Guerra, referiu que o contrato ainda no estava assinado, e que a empresa tem tido um comportamento impecvel com a Cmara. +P. -- Pode querer fomentar a criao de um grupo financeiro, tal como o Banif pretendeu fazer. +R. -- At agora, a poltica do Governo para as privatizaes tem sido orientada de modo a obter o melhor encaixe financeiro. +A forma de o conseguir pr venda as aces que ainda detm nas empresas nas melhores condies. +No existem motivos para alterar, agora, a sua estratgia. + um homem alto e magro, mos de dedos esguios e gestos largos a abraar desenhos, projectos, frases. +Numa voz grave, falou sobre o Oceanrio de Lisboa e depois, descontrado, perdeu-se a contar coisas dos tempos da infncia e juventude, os caminhos que foi tomando e que o levaram a ser considerado o maior especialista do mundo em grandes aqurios. +Depois de ter feito cinco grandes aqurios e muitas outras obras de dimenses monumentais, confessa que a coisa mais importante que fez foi mexer no metropolitano de Boston, remodelar toda a rede, torn-la agradvel e simples de entender. +Fala aqui um pouco de tudo, das cobras da infncia em Nova Iorque s preocupaes sobre a corrida aos armamentos nos anos 60, do prazer de ver golfinhos a brincar livremente, de vulces e glaciares a esculpir a paisagem da Terra. +Ao falar de Portugal, comea subitamente a trautear um fado de Amlia, de um disco que comprou h muitos anos, no sabe onde nem porqu, e que gosta de ouvir e de cantar sem saber que palavras so aquelas. +Um destes trgicos erros foi o maniquesmo. +Este fazia-nos acreditar que os nossos eram filhos da luz e os adversrios, das trevas. +Pensvamos que tudo o que nos diziam era razovel e verdadeiro, mesmo sem o ter meditado, e que os nossos adversrios estavam sempre enganados. +Nessa base, propnhamos projectos no global, com os quais pretendamos resolver todos os problemas do pas. +Foram, principalmente, as atitudes sectrias e exclusivistas que nos levaram derrocada democrtica, entre outros factores. +Em 1993, pensamos de forma diferente. +Procuramos acordos, tratamos de conciliar posies, estamos receptivos para compreender e para aceitar critrios diferentes dos nossos. +A criao de um observatrio com indicadores de convergncia, que dever abranger mais de uma dezena de sectores, um dos projectos a concretizar com a nova fase de intervenes estruturais da Comunidade. +Atravs deste rgo, que funcionar como base estatstica, as autoridades portuguesas pretendem acompanhar o percurso de convergncia de cada um dos sectores, com base em trs grandes grupos de indicadores: estado de situao, performance conseguida e investimento. + Alm do estudo dos sismos e das caractersticas tectnicas das regies onde eles tm lugar, as exploses nucleares podem ainda servir outros fins. +De facto, h muitos anos que h quem proponha a utilizao de pequenas bombas nucleares em certas grandes obras de engenharia como terraplanagens de montanhas ou abertura de albufeiras. + Recorde-se alis que, em Dezembro de 1992, quando a China realizou a maior exploso no nuclear de sempre -- arrasando a montanha Paotai, situada na Zona Econmica Especial de Zhuhai, na ilha de Sanzao, 25 quilmetros a sudoeste de Macau --, na qual foram gastas 11 mil toneladas de dinamite, no faltou quem especulasse sobre a possibilidade de um dia realizar obras desse tipo com engenhos nucleares. +A ex-Unio Sovitica, que levou a cabo um ambicioso programa de exploses nucleares subterrneas pacficas durante anos, parece ser a regio do globo onde este tipo de know-how foi mais desenvolvido, ainda que se desconhea o nvel actual de domnio desta tecnologia. +Ainda que a grande preocupao do pblico se costume situar ao nvel da radioactividade libertada, no caso das exploses subterrneas os especialistas dizem que ela pode ser facilmente controlada, havendo, porm, o perigo de se contaminar lenis subterrneos de gua. +Por outro lado, o que alguns receiam que uma forte exploso deste tipo possa provocar, ao nvel das placas, fragilidades que venham a tornar mais graves as consequncias de um futuro sismo natural. +trata-se das Laffer Utilities (Laffer o nome de famlia do heri), apresentadas como um conjunto de programas para perder tempo quando devia era estar a trabalhar. +Algures nos textos do programa l-se que ele capaz de automatizar tudo o que voc faz no escritrio e no tem que ver com trabalho. +Com uma base de dados de clip-art, um programa de impresso de apostas, uma seleco de anedotas temticas, um criador de cartazes e algumas surpresas mais, as Laffer Utilities so uma boa maneira de ... perder um emprego. +Lembrar as razes foi ainda preocupao de Al Lowe ao preparar este disco, por isso mesmo, a par com alguns textos seus sobre os anos passados a criar Larry, uma divertida entrevista em vdeo, Al Lowe Undubbed, para a televiso alem -- e um vdeo e demo interactivo de Freddy Pharkas, um jogo de All Lowe -- h ainda espao para lembrar o jogo que est na origem do azarado heri da srie: Softporn Adventure. +O texto refere-se, nomeadamente, s especificidades destes reclusos e constata que a comunidade prisional, intermediria da sano social atravs da privao da liberdade, passou a acolher uma populao difcil que ora se revolta, ora se adapta, ora se refugia na doena fsica ou psicolgica. +E que efeitos se podem esperar da aplicao da pena de priso queles que cometeram delitos pela exigncia da toxicodependncia? +--interroga a diviso de estudos e planeamento da Direco Geral dos Servios Prisionais. +Bucareste acusou a Hungria de ter pretenses territoriais na Transilvnia -- uma regio atribuda Romnia em 1920, aps o fim do Imprio Austro-Hungaro -- e de apoiar a concesso de autonomia s regies dominadas pelos magiares romenos, uma vez que esta legitimaria a sua ingerncia nos assuntos internos de outro pas. +Para mais, o Governo hngaro recusou at agora a assinatura de um acordo sobre a intangibilidade das fronteiras dos dois pases vizinhos. +O dilogo poltico a nica via para melhorar as relaes entre a Hungria e a Romnia, afirmou quarta-feira Teodor Malescanu. +Mas as palavras do ministro romeno foram recebidas pelos responsveis da UDMR como uma jogada de oportunismo. +Isto porque o problema da minoria hngara denegriu a imagem internacional da Romnia, que tenta a todo o custo fazer aprovar o seu pedido de adeso ao Conselho da Europa (CE), que ser votado no incio de Outubro. +O objectivo do acordo consolidar o cessar-fogo assinado o ms passado, e proceder troca de prisioneiros entre os dois campos. +A guerra civil no Tadjiquisto, que fez mais de 50 mortos, comeou em 1992, quando as foras do neo-comunista Rakhmonov derrubaram o governo dos islamistas, que, nas ltimas semanas, tm somado vitrias e se aproximam perigosamente da capital. + No Dubai, as vedetas candidatas a dividir um bolo de um milho de dlares so Goran Ivanisevic, Thomas Muster e Boris Becker. +Mas as atenes sero certamente divididas com a armada espanhola, composta por Carlos Moya, Albert Costa e Flix Mantilla. +Em Marselha (539 mil dlares), os favoritos so Marcelo Rios, Thomas Enqvist e Michael Stich. +Por c, prossegue a segunda etapa do circuito satlite CESAE nos courts rpidos da Associao Acadmica de Coimbra. +Dos portugueses presentes no quadro final, apenas dois actuaram ontem e ambos foram eliminados. +O campeo nacional Bruno Fragoso perdeu com o eslovaco Boris Borgula por 6-1, 7-6 (7-3) e Tiago Vinhas de Sousa foi derrotado pelo brasileiro Mrcio Carlsson por 6-2, 6-0. +Contudo, as alteraes no se ficam por aqui. +Para conseguir que a originalidade volte ao local, o arquitecto quer ainda restaurar a chamada Casa do Nuno (onde habitava Nuno, o filho de Camilo) e onde actualmente funciona a sede da junta e o posto mdico. + Apesar da discusso que estas obras esto j a merecer na freguesia, o restauro da igreja paroquial , sem dvida, o assunto de que mais se fala. +Desde h j alguns anos que chove a eito na centenria Igreja de Ceide. +O ltimo Inverno foi j muito difcil de passar e, recordam os paroquianos, que chovia tanto dentro da igreja como c fora. +O padre Gabriel Pereira Lopes, proco da freguesia, queixa-se tambm do facto da igreja ser muito pequena e por isso, e para alm do programado restauro, entrou na Cmara de Famalico um proposta para a ampliao do templo. +A Comisso de Obras da Igreja de Ceide quer alargar o edifcio de uma forma que no ter agradado a Siza Vieira. +O arquitecto esteve j, por vrias vezes, no local e ter manifestado a sua oposio modificao da estrutura da igreja, defendendo antes a manuteno do actual edifcio. +Mais quatro pessoas foram mortas ontem num bairro pobre de Istambul, durante os recontros entre manifestantes alevitas e a polcia, pouco antes de representantes de uns e outros terem chegado a um acordo pouco auspicioso. +As ltimas mortes de que h notcia ocorreram no bairro de Umraniye, na margem asitica da maior cidade turca, onde cerca de 1500 alevitas saram para a rua para protestar contra a interveno da polcia num outro bairro, Gazi Mahallesi, onde nos ltimos trs dias morreram entre 17 e 23 pessoas em manifestaes. +E, em sntese, remeteu para o seu advogado, Lus Avides Moreira. +A posio de Brando sustenta-se numa carta da Lacto-Lusa, de 6 de Agosto de 1993, que anula o contrato com a Socifa. +Inteno que foi aceite pela sociedade, conforme carta do dia 16. +E alarga o rol dos lesados: os credores da Socifa e o prprio fisco, por no pagamento do IVA devido na prestao de um servio. +Silncio sepulcral, s os pssaros se ouvem. +Mas Manoel de Oliveira continua a ouvir rudos. + um barco que vai a passar no mar, murmura um assistente. +Aguarda-se que o barco se confunda ao longe com o horizonte .. +Sustm-se as respiraes. +Ser desta? +Malkovich (Michael) e Deneuve (Hlne) so um casal -- ele americano, ela francesa -- que vem a Portugal porque Michael, historiador, julga que pode encontrar na Arrbida documentos que lhe comprovem uma tese segundo a qual Shakespeare era um judeu de origem espanhola. +No convento, o casal depara com alguns estranhos personagens: Baltar -- Lus Miguel Cintra --, o director dos arquivos; o seu assistente, Baltazar -- Bnard da Costa --, e a cozinheira -- Elosa Miranda. +Para os cubanos, o Che um semi-Deus vindo de outro lado que quis fazer deles homens novos inventando o trabalho gratuito, a abnegao poltica, o internacionalismo. +Mas que resta hoje destes sonhos? + Um monte de boas intenes abandonadas junto dos caixotes de lixo das cafetarias em dlares, onde jovens e velhos vasculham, ou pisadas pelos saltos-agulha das centenas de jineteras (prostitutas) do Malecn e da Quinta Avenida. +Aos 15 anos, muitas crianas do Che deixam a escola primria e encontram-se na rua para buscarse la vida de qualquier forma. +Para os antigos, o Che est prestes a morrer uma segunda vez. + preciso um meio dia de trabalho a um operrio para ganhar a famosa nota vermelha de trs pesos com a efgie do Che que lhe permitir comprar trs bananas no mercado livre campons. + O dlar e o marco fixaram respectivamente a 159.775 e 87.35 escudos face aos 156.942 e 86.871 verificados na sesso anterior. +Este ano vai ser aberto um concurso para projectos de investigao sobre a desigualdade entre homens e mulheres na sociedade portuguesa. +O objectivo conhecer melhor os fenmenos e as tendncias da desigualdade entre sexos, para que esse conhecimento possa servir de base tomada de decises polticas. +Temos um exemplo logo aqui ao lado na nossa vizinha Espanha, onde os jogadores recebem bem, mas jogam futebol. +Como bonito ver os estdios cheios e os espectadores verem bom futebol. +C em Portugal, o que vemos? +Fraudes, mortes nos campos de futebol, rbitros que so tendenciosos a favor de quem querem e outras coisas mais que s trazem vergonha ao nosso futebol. +A polcia efectuou, na manh de ontem, nada menos do que oito detenes no Bairro de S. Tom, em Paranhos, na sequncia de vrias buscas domicilirias. +A operao policial, que teve incio cerca das 9h00, visou detectar indcios de trfico de estupefacientes. +Num dos domiclios, os agentes detiveram trs homens (um estucador de 56 anos, um padeiro de 19 e um empregado de balco de 24) e trs mulheres (uma reformada de 50 anos, uma estudante de 16 e uma tecedeira de 22), aparentemente da mesma famlia, tendo sido ainda apreendida uma aprecivel quantidade de droga. +a outra rota por barco para portos como Southampton, Roterdo e Anturpia. +ser correcto transaccionar fsseis com coleccionadores particulares, enquanto os paleontlogos (envolvidos muitas vezes em descobertas importantes) esto impedidos de fazer dinheiro com os seus achados? +No poder esta situao levar corrupo da classe? +Esta a altura ideal para discutir tica, disse ao PBLICO Lawrence Flynn, da Universidade de Harvard. +O PBLICO apurou tambm que as negociaes entre a Marconi e a CN sobre a redistribuio das participaes estatais j esto bastante avanadas. +A reestruturao do quadro accionista na TMN dever constituir um dos primeiros passos. +A Telecom Portugal poder vir a assumir uma participao significativa no operador de telemveis -- ainda que inferior a 50 por cento -- e, em contrapartida, a Marconi ter manifestado, mais uma vez, o seu interesse no trfego internacional para a Europa. +Actualmente, o capital da TMN est distribudo, em trs partes iguais, pela Telecom, pela TLP e pela Marconi. +A assembleia geral da Marconi deu tambm luz verde emisso de papel comercial por parte da empresa -- que dever acontecer a breve prazo --, para alm de ter deliberado no sentido de os ttulos da companhia passarem a revestir a forma de aces escriturais, de modo a permitir uma maior liquidez do papel. +Mrio Carrascalo desempenhou as funes de governador de Timor-Leste de 1982 a 1992. + Nessa altura, por causa do massacre do cemitrio de Santa Cruz, descolou do regime de Jacarta, passando a manifestar algumas posies crticas, nomeadamente em relao s foras de segurana. +Foi substitudo por um timorense bastante mais pr-Indonsia, o actual governador Ablio Osrio Soares. +As crticas custaram-lhe o exlio, tendo sido nomeado embaixador da Indonsia na Romnia. +Em 1997 regressou a Timor-Leste. +Reencontrou-se com o seu irmo Manuel, tambm ele um timorense pr-integrao que progressivamente foi caminhando ao encontro das posies da resistncia. +numa atitude indita, Robin Cook telefonou ao seu colega portugus antes de embarcar para Jacarta, para falar sobre Timor. +Um dos assuntos abordados nessa ocasio foi a venda de armamento britnico ao regime de Jacarta. +Nos meios diplomticos h, alis, quem interprete estas manifestaes de sensibilidade em relao questo de Timor por parte do Executivo de Tony Blair com uma tentativa de compensar a m imagem criada com as significativas exportaes de armas made in England rumo Indonsia. +Apesar de ter herdado do Executivo de John Major as mais polmicas licenas de exportao de armas para Jacarta, o Executivo trabalhista no escapou s crticas dos activistas pr-direitos humanos, que acusam os novos governantes de no terem travado os negcios, nomeadamente a venda dos caas Hawk fabricados pela British Aerospace e alegadamente utilizados em Timor. +No entanto, apenas trs meses depois da entrada em vigor do novo horrio de funcionamento do Mercado de Manuel Firmino, que estipulava um novo perodo de abertura entre as 17h00 e as 20h00, foi decidido voltar a encerrar o mercado da parte da tarde. +A reivindicao partiu de alguns comerciantes, desagradados pelo facto de a abertura do mercado tarde no ter atrado novos consumidores. +E nem a campanha publicitria desenvolvida em rgos de comunicao regionais, promovendo o mercado e dando conta do alargamento do seu horrio de funcionamento, levou mais pessoas ao Manuel Firmino. +O fracasso desta medida ditou que, desde o incio deste ms, o mercado voltasse a funcionar apenas da parte da manh, encerrando portas s 14h00. +Um dos comerciantes no poupa crticas Cmara, considerando que a campanha publicitria foi mal conduzida, que as obras esto atrasadas e que a alterao do horrio deveria ter sido feita aps a concluso das obras. +Nem sequer puseram ' placards ' pela cidade, desabafa este comerciante, que considera que os anncios do alargamento do horrio da parte da tarde deveria ter sido mais publicitado. +E acusa ainda a Cmara Municipal de no ter investido nos ltimos anos no Mercado de Manuel Firmino, por pretender que os comerciantes se transfiram para o novo Mercado Municipal de Santiago. +O facto causou a maior indignao junto dos estudantes daquela faculdade, cuja associao de estudantes se apressou, alis, a colocar cartazes em todos os locais da escola avisando os recm-chegados mais distrados para que no passem cheques em branco. +Apesar do grotesco da situao, qualquer caloiro que procurasse saber das diligncias que necessita de efectuar para se inscrever em Cincias, deparava com uma longa lista de preceitos, intitulada Aviso e que explicava que todos os colocados na faculdade no ano lectivo de 1994/95 (1 ano/1 vez) faro a sua matrcula por via postal (correio registado), ao que se seguia uma listagem dos documentos a enviar. +A meio da lista, era solicitado o envio de um cheque emitido a favor a Faculdade de Cincias da Universidade de Lisboa, devidamente identificado com o nome do aluno no verso, para pagamento de propinas. +De montantes, nada vem referido. +Ora, conforme explica Nuno Bio, elemento da Associao de Estudantes, ainda no est fixado o valor das propinas a pagar pelos estudantes, ainda no se sabe quem ter direito a reduo, o regulamento interno da faculdade ainda nem sequer foi elaborado. +Alis, aos alunos-caloiros era tambm pedido o envio do requerimento de reduo de propinas se pretender apresent-lo. +Nuno Bio no tem dvidas de que se trata de uma tentativa frustrada por parte dos orgos competentes da faculdade de obrigar os caloiros, menos informados destas coisas, a pagar, para poder afirmar depois que h muita gente a pagar propinas. +Um dos objectivos de Portugal nas negociaes do Uruguay Round do Acordo Geral de Tarifas e Comrcio (GATT) poder ter uma soluo positiva, dada a proposta que os Estados Unidos ontem apresentaram. +Trata-se da aprovao de um perodo de 15 anos para a abertura das barreiras alfandegrias ao sector txtil. +A Teleweb, uma nova empresa portuguesa fornecedora de acesso Internet, anunciou estar disposta a investir um milho de contos de forma a atingir o seu objectivo de 50 mil clientes no ano 2000. + A empresa totalmente detida pela Finantel SGPS SA, que detm participaes na Ensitel e na NCM-Nokia e atingiu em 1997 um volume de vendas consolidado de 9,2 milhes de contos, com resultados lquidos de 244 mil contos. + Andava pela rua de cabea perdida. +Com cinquenta e trs anos, dizia ele de si para si, portava-se como um garoto. +Que vergonha! +Mas no via nenhuma sada. + No momento em que chegou ao jardim pblico da rua Au-Sable viu, numa lea deserta, uma silhueta magricela. +Aproximou-se e o corao comeou a bater com fora. +Era mesmo o Pierre-Jean, da outra vez. +Subiram-lhe as lgrimas aos olhos, e por pouco no beijou o seu antigo conhecido.Subiram-lhas lgrimas aos olhos, e por pouco no beijou o seu antigo conhecido. +O outro esquivou-se delicadamente, mas convidou-o a subir a bordo. +Aparentemente imparvel nos seus projectos de expanso (para grande dor-de-cabea das marcas europeias) o anncio da Honda surgiu uma semana depois do acordo conseguido entre o Japo e a Comunidade Europeia para a abertura gradual do mercado aos carros nipnicos a partir de 1992, e dois dias depois da morte do seu Oyaji-san (o grande pai), o homem que via na Europa o grande mercado a conquistar. +Soichiro Honda foi o homem que construiu um imprio, que encarna o renascimento da indstria japonesa no ps-guerra, desde a pequena fbrica de motocicletas, em 1947, ao grupo que factura hoje 4,5 mil milhes de contos e tem 13 milhes de contos de lucros. +A par do seu envolvimento com empresas de efeitos especiais, como o caso da ILM, alguns projectos recentes contriburam para dinamizar a companhia. + o caso da associao com a Time Warner Cable para a concretizao da TV interactiva em Orlando (na Florida), com a Nintendo para a criao do novo sistema de videojogos Ultra 64, ou com a Sprint of the Drums para criar uma rede de alto dbito para a indstria do entertainment produzir filmes ou material de publicidade. +Com a AT&T, criou a joint-venture Interactive Digital Solutions para o desenvolvimento e oferta de solues de vdeo interactivo, servios de informao ou de entretenimento, atravs das redes telefnicas e de TV por cabo. +Foi tambm a primeira empresa nomeada pela companhia telefnica japonesa NTT para proporcionar servios interactivos na rede digital que este operador est a instalar. +Com sede em Mountain View, na Califrnia, a sua presena em feiras ou conferncias internacionais usual e, no Siggraph deste ano (a 21 edio do maior certame actual de informtica, com 25 mil visitantes), foi o expositor com mais espao. +A SGI aproveitou para anunciar a criao de uma nova empresa -- a Silicon Studio -- para o desenvolvimento de aplicaes para os novos mercados de ' media ' digital. +A nova empresa trabalhar com programadores, criadores artsticos e distribuidores na expanso das suas actividades correntes nos domnios do filme, vdeo, TV interactiva, vdeojogos ou parques de diverses. +Crimos a nova subsidiria para promover o crescimento da SGI no mercado do ' entertainment ', referiu revista Post Update Mike Ramsay, presidente da Silicon Studio, que diz pensar que a tecnologia para a rea do ' entertainment ' comandar o mercado nos prximos cinco anos. +Documentos internos da empresa apontam o mercado da TV interactiva e dos videojogos como aqueles que, a longo prazo, vo ter uma maior procura, seguindo-se ento o vdeo e o filme digital e, por ltimo, os efeitos especiais. +A Renault Portuguesa tem actualmente em curso um processo de despedimento colectivo de 144 trabalhadores. +Os administradores nomeados pelo Estado portugus votaram contra esta deciso, mas os franceses, maioritrios, decidiram ir em frente, o que constitui mais uma prova do confronto aberto instalado na empresa. +Como os despedimentos ainda no foram consumados, a Renault continua com 765 trabalhadores e com uma produo diria de 160 automveis do modelo Clio. +A administrao da fbrica j fez no entanto saber que sua inteno reduzir a produo diria para 110 unidades. +Entretanto, os trabalhadores da fbrica de Setbal realizaram uma greve na quinta-feira passada e marcaram nova paralisao para a prxima segunda-feira, que incluir uma marcha sobre Lisboa. +A administrao francesa chegou a defender a venda da fbrica de Setbal a outro construtor automvel. +O Governo portugus desenvolveu alguns contactos nesse sentido, e os coreanos da Kia chegaram mesmo a deslocar-se s instalaes. +Mas as autoridades nacionais no viram grande receptividade e decidiram afastar essa hiptese, porque no queriam ficar com o nus de no conseguirem arranjar comprador, o que constituiria um trunfo para os franceses. +Na concertao, h ainda muito a fazer, constatou Maria Jos Constncio, que prometeu o seu empenhamento pessoal para que a falta de concertao entre as entidades locais e o governo central deixe de ser um ponto fraco do PDI do Vale do Ave. +H que coordenar a aco dos diversos ministrios e municpios. +Em Portugal, infelizmente, os diversos ministrios e as autarquias tm os seus programas, mas, por vezes, no tm uma coordenao rigorosa, advertiu. +Para evitar uma aplicao desgarrada de milhes de contos no Vale do Ave, Cravinho preconiza um sistema de gesto forte, que ponha todos a trabalhar de forma articulada para uma boa aplicao das verbas. +Do lado portista, o dia foi calmo e sem problemas, entre Montechoro e as Aoteias, em cujo complexo desportivo a equipa fez ontem tarde o ltimo treino antes da partida. +Tranquilidade absoluta, nenhuma hostilidade da parte de ningum, uma paz realmente sem mcula. +Reinaldo Teles, que chefia a comitiva o nico dirigente presente e no se espera a presena de mais nenhum dos altos responsveis. +Como o jogo d na televiso julgo que no vem mais ningum, disse-nos o homem que dirige o departamento de futebol portista. +O deputado socialista Ferro Rodrigues mostrou-se descontente com as respostas do Governo, que considerou vagas e evasivas. +O deputado disse mesmo que o o Governo est, deliberadamente, a querer desvalorizar politicamente um problema que um escndalo financeiro gravssimo que envolve muitos milhares de contos em fugas ao fisco. +Tudo isto porque a resposta ao Grupo Parlamentar Socialista sobre esta matria foi dada pelo subsecretrio de Estado adjunto da secretria de Estado do Oramento, Vasco Valdez. +Quando espervamos a presena do ministro das Finanas, Braga de Macedo, surgiu no o ministro, nem a secretria de Estado adjunta e do Oramento, mas sim um subsecretrio de Estado adjunto da secretria de Estado adjunta do ministro adjunto do primeiro-ministro, referiu o deputado socialista. +Para Ferro Rodrigues, no se trata de uma questo de competncia, mas sim da forma como o Governo est a tratar este assunto. +Palcio Galveias. +Campo Pequeno. +3 a 6, das 10h s 19h. +Sb., e dom., das 14h s 19h. +Um conjunto de grandes telas, em que a exuberncia cromtica pretexto realizao de obras simpticas, precede a apresentao de um conjunto de estudos para os painis do Metropolitano de Lisboa -- entendidas as paredes como suporte de pintura, no sentido que possua antes de Maria Keil o transformar. +no so todos os que intitulam um quadro Um co, talvez. +Enquanto isto, foi ontem anulada a misso governamental angolana que deveria ter ido Jamba, quartel-general da UNITA, recolher parte dos presos de guerra. +Abel Chivukuvuku, membro da Comisso Conjunta Poltico-Militar (CCPM) para a fiscalizao do cessar-fogo, alegou imprensa razes tcnico-logsticas para a anulao da viagem, relacionadas quer com o nmero reduzido de presos que desejaria voltar para Luanda quer com a programao da cerimnia. +O silenciamento das emisses da RTPi na Guin-Bissau, aqui noticiado ontem e sbado, vem recolocar a questo das complexas relaes de Portugal com as suas antigas colnias. + Um fantasma que se arrasta nestes 20 anos ainda marcados por uma descolonizao que foi -- ou teve de ser -- o que foi, verdade, mas que importa exorcizar de uma vez por todas. +Sem complexos, mas com a coragem de um projecto srio, consequente e responsvel ao nvel da cooperao, em geral, e do audiovisual em particular. +Foi o que no aconteceu, mesmo nestes ltimos tempos de obra feita do cavaquismo. + R. -- O Presidente tem um prazo at Maro para o assinar e apresentar ao Congresso, como um pacote que dever ser aceite ou rejeitado. +Caso ultrapasse esse prazo, o Congresso poder discutir cada alnea, o que pode adiar indefinidamente a sua aplicao. + possvel que quem apresentou essa ideia considere que seja melhor para os Estados Unidos e para o mundo ter qualquer acordo do que no ter nada. +O trabalho em famlia dos Hestons pai e filho (Charlton, o intrprete, e Fraser, o realizador) revelou-se bastante interessante na Ilha do Tesouro que h pouco vimos e justifica o interesse pela reincidncia, agora volta de outra figura clssica: Sherlock Holmes, investigando a desapario de um militar britnico, o que o leva a uma intriga de cobia e vingana. +Legal Vdeo, 1992, 101 min. +O painel considerou que os empreiteiros teriam menos tendncia para relatar eventuais problemas de segurana e exortou a NASA a manter a sua presena nas fbricas dos fornecedores. +A NASA no se deve iludir pelo aparente sucesso inicial de todos os esforos de transio, sublinha o documento. +O administrador da NASA, Daniel Goldin, ao fazer a sua leitura do relatrio afirmou que este tinha considerado que o programa de vaivns se encontrava de excelente sade. +O painel tambm aponta algumas reas onde preciso continuar a colocar alguma nfase e outras onde necessrio melhorar, disse Goldin. +A NASA concorda com essas afirmaes e j dei instrues para que sejam postas em prtica o mais depressa possvel. +S recentemente e de uma forma restritiva, alguns tribunais comearam a considerar que os processos contra os jornalistas devem ser suspensos at que estejam decididos os processos contra aqueles, geralmente figuras pblicas, de quem falaram. +A lei de imprensa, na sua actual verso, nestes como em muitos outros aspectos, fere gravemente o direito de ser informado dos portugueses, pelo que se aguarda que seja brevemente discutido na Assembleia da Repblica um novo projecto de lei de imprensa que confirme o reforo da liberdade de expresso, de informao e de imprensa. +Orlando Miguel e Jos Pedro partiram de bicicleta, levaram sacos-cama, roupa, comida e algum dinheiro. +O primeiro deixou um bilhete aos pais dizendo que no se preocupassem e que no ia fugir. +Foram cerca de 500 jovens atletas, representando escolas e clubes de vrias regies do pas, incluindo a ilha da Madeira, que participaram no III Encontro Nacional de Andebol Feminino, j considerado o maior da modalidade, organizado pela Associao de Andebol de Lisboa. + As expectativas dos promotores no tero sido goradas e a palavra sucesso era repetida, com insistncia, pelos organizadores. +Creio que o encontro foi muito proveitoso. +Para alm do convvio, as jovens atletas puderam praticar o seu desporto favorito e mostrarem as suas capacidades, afirmou ao PBLICO Isabel Cruz, da Associao de Andebol de Lisboa. +3 milhes de contos. +Robert Koch, um Don Juan norte-americano de 51 anos, foi desmascarado e preso depois de ter burlado mais de 200 mulheres com promessas de casamento. +Fazia-se passar por um vivo rico, propritario de fbricas e sedento de carinho, e, depois de conseguir que as suas presas lhe confiassem grandes quantias em dinheiro e jias, desaparecia sem deixar rasto. +Reconhecendo que ningum melhor que os prprios sabem o que deve ser feito, Nery pensa que deve haver uma separao clara entre subsdios a equipamentos, subsdios produo e aos custos permanentes de manuteno de uma companhia. +Exigncias que obrigam os grupos a montar peas a correr s para no serem penalizados monetariamente -- o que no saudvel para ningum, muito menos para o Gil Vicente. + Renitente em divulgar quais sero os novos critrios -- embora tenha algumas ideias gerais --, Nery espera pelos resultados dos inquritos. +Certo que ser um jri que decidir a atribuio dos subsdios e que dever incluir criadores no envolvidos no concurso, crticos e outros profissionais ligados ao sector. +Vieira Nery julga que as decises devero ser tomadas por uma comisso mista que envolva o Estado e avaliadores exteriores. +Mas no acredita no convite dos nomes pelos nomes. +Uma fita carregadora com 20 cartuchos de bala real para espingarda G-3, 17 balas de salva, 27 invlucros, uma granada de mo defensiva M-63, uma granada ofensiva M-62, uma granada de fumo, uma munio para espingarda Mauser e uma munio de 20 milimetros, constituam o armamento abandonado sem qualquer tipo de sinalizao. +O material, depois de inspeccionado pela GNR de Lamego, foi entregue no Centro de Instruo de Operaes Especiais. +A comear em Lou Reed, passando por David Bowie e de Iggy Pop, h toda uma tradio de estrelas anglo-saxnicas em fase de crise existencial, que acabam por ir parar a Berlim, onde vm a gravar discos de um pessimismo to cerrado quanto brilhante. +Apesar de o Muro ter cado, os U2 trataram de revitalizar esta tradio num disco de uma negritude sem par na sua discografia prvia que, se no constitui uma ruptura to radical com o passado quanto The Unforgettable Fire. +No entanto, por esse mesmo estado de esprito que o anima, um disco diferente na carreira do quarteto irlands. +As letras simplificam-se, para trazer flor da pele uma amargura infinita, enquanto os arranjos denotam um frenesim recalcado, a que a produo de Lanois, Eno e Lillywhite vai acentuando as nuances dramticas. +Achtung Baby arte pop no seu znite, isto , beira do hospital psquico. +Os adeptos da face mais ligeira da colaborao de Kurt Weil com Bertolt Brecht devem odiar esta reviso hard-core do seu reportrio, como as incurses dos Young Gods pela msica de circo e vaudeville no devem ter feito a felicidade dos adeptos da sua vertente mais industrial. + um disco pouco indicado para amantes de ortodoxias, o que, de modo algum, o torna inconsistente, tratando-se afinal de uma vanguarda actual, que revisita luz dos seus prprios princpios outra vanguarda, que a antecedeu sob vrios aspectos. +O resultado brutal e espectacular, baseado em verses radicais do reportrio mais famoso de Weil / Brecht, repletas de descargas de electricidade e avalanches de rudos urbanos, que da estrutura musical da famosa dupla no deixam mais que o esqueleto, mas lhe acentuam a sua carga teatral e inerente dramatismo, ao ponto do sufoco. +O Sindicato dos Maquinistas dos Caminhos de Ferro Portugueses convocou mais uma greve s horas extraordinrias a partir de segunda-feira, informou ontem o porta-voz da CP. +H algo de inacabado, amputado na nossa cultura, especifica Antnio Jos Saraiva, uma espcie de infncia para alm do seu termo. +Foi isso que nos levou procura de outro pai alm-Pirenus, na CE. +Uma segunda linha a que engloba os tecnocratas de optimismo standardizado e maniqueu, com a sua subcultura do sucesso, do consumismo, do top, da produtividades, da eficcia. +No decurso da conferncia de imprensa, Carvalhas quis sublinhar a conjuntura em que est a ser preparado o Congresso. +Decorre num quadro em que cresce o desencanto, a frustrao e o protesto dos trabalhadores e de amplos sectores da sociedade pelas consequncias de uma poltica que nos seus principais eixos no se afasta daqueles que constituram a matriz poltica que os portugueses quiseram ver derrotada. +Carvalhas -- que continuar a assegurar a liderana do partido -- manifestou ainda a sua convico de que o Congresso ser um importante impulso para o reforo do PCP. +A ideia de instalar na frente costeira da margem direita do Douro um equipamento virado para o mar, embora longe da dimenso que se pretende agora para o Centro de Cincia e Tecnologias do Mar, tem quase 20 anos e as diversas tentativas efectuadas at hoje nunca chegaram a bom termo, estivesse no Porto ou em Matosinhos o local escolhido. +Tudo comeou com a ideia, em 1980, de substituir a j ento degradada Estao de Zoologia Martima Augusto Nobre, ou Aqurio da Foz, como mais conhecido, encerrado h mais de 30 anos. +1980 -- Por iniciativa de um bolseiro alemo, Michael Weber (hoje docente convidado do ICBAS), os rotrios do Porto ponderam duas ideias: reconstruir o velho aqurio da Foz, ou construir, de raiz, um novo aqurio, no Porto. +Cmara do Porto, Universidade e Administrao Central aderem ao projecto. +O primeiro local apontado so os terrenos do futuro parque da cidade. +No entanto, o consumidor poderia optar pelo envio de um cheque sobre o estrangeiro passado pelo banco do comprador em moeda do pas onde se vai fazer o pagamento -- a forma mais barata --, ou passar um cheque no pas da compra em moeda local. +Cinco dias depois Pedro Conceio deslocou-se cidade com os documentos indispensveis para avanar com o processo de matrcula (bilhete de identidade, passaporte e nmero de contribuinte). +No dia seguinte, o carro podia ser levantado, tendo sido efectuado o pagamento de cerca de 1.600 contos em cheque sobre o estrangeiro. +Com o carro, foi entregue uma guia de circulao vlida para 90 dias em territrio espanhol, o livrete, uma factura de compra e uma aplice respeitante a um seguro contra todos os riscos vlido por 30 dias (custo: 38 contos). +Foi uma deciso muito ponderada, muito difcil e muito amadurecida, disse ao PBLICO Anabela Moutinho, que foi lacnica no que se refere aos motivos. + No cargo apenas deste Outubro, ela assegurava, entre outras coisas, a ligao entre o IPACA e os espaos potenciadores de novos pblicos, como os festivais de cinema, nacionais e internacionais, as escolas e os cineclubes. +Um projecto que ser concretizado por meio de protocolos, mas que Anabela Moutinho no teve tempo de concretizar por falta de tempo til. +Esta uma poca de transio, muito importante e difcil para o sector, admitiu. +O PBLICO tentou saber se havia divergncias de pontos de vista entre Anabela Moutinho e Costa Ramos, que ela no confirmou nem desmentiu. +A minha deciso no foi pacfica, no seria honesto da minha parte diz-lo, posso dizer que foi pacfica e sem equvocos a maneira como o processo aconteceu, declarou. + O autor receava, aquando da sua publicao, que este livro fosse incompreendido. +Razes? +Em particular, a linguagem simblica, certamente hermtica que foge ao estilo que mais tarde escolhi, justifica-se numa nota de contracapa. +A ausncia da pretenso didctica, inequivocamente pautada em livros subsequentes, que narram igualmente a saga dos angolanos, como Mayombe ou As Aventuras de Ngunga, s aparente, porque esta componente, embora aqui se afigure numa estratgia enunciativa bastante esquiva, est flagrantemente presente. +Quisera acabar com a ovalidade do mundo e conseguira. +Mas o quebrar do sonho aliou-se impossibilidade de viver no mundo sem ovalidade. +E ainda no havia mquinas que realizassem os sonhos individuais. +S os de grupos. +(pg. 158) Poder haver uma sugesto maior? +Ela tentou olhar o lado esquerdo, mas uma montanha a separava. +Ele fez o mesmo para o lado direito, mas a mesma montanha o impedia. + Cada um contemplou o seu lado, reconhecendo-se, incapaz de transpor a montanha (pg. 11). +-- Quando os corvos forem derrotados, no ser s aqui na montanha que o Sol ser azul. + Por toda a parte ele dardejar rosas sem espinhos ... -- dizia ele. +E ela sorria quela verdade desejada. + -- Os meninos brincaro com o vento da madrugada, com ele fixando o capim terra ... +-- E os morcegos comero mel e no excrementos ... -- concluiu ela (pg. 51). +Este assunto, alis, foi tambm discutido na CCPM, que quinta-feira efectuou a sua 27 plenria, aps um interregno de 20 dias. +O caso Quilengues, em que morreram trs turistas britnicos e um neo-zelands, foi um dos assuntos em discusso, dada a contradio entre as partes. + A UNITA apresentou recentemente um suposto chefe do grupo que assassinou os turistas e que seria um oficial do ex-Ministrio da Segurana de Estado. +As declaraes do prisioneiro no convenceram ningum e o Governo rebate que a zona de Quilengues de acantonamento de tropas da UNITA. +Vrias pessoas citadas como tendo participado no acto desmentiram as verses da UNITA, e a Comisso Mista de Verificao e Fiscalizao (CMVF) encarregada de esclarecer o assunto no conseguiu apurar nada por dificuldades em chegar ao local. +Duas organizaes francesas, a Associao Fora Operria Consumidores (AFOC, Altos Alpes) e a Unio Departamental das Associaes Familiares (UDAF), assinaram um acordo de cooperao com associaes de consumidores italianas que levou criao de uma estrutura franco-italiana denominada Consumidores sem Fronteiras. +Esta organizao constituir, segundo os seus promotores, a base de uma nova agncia de informao europeia aos consumidores, a inaugurar no prximo ms de Junho. +Os Consumidores sem Fronteiras propem-se, para j, envolver-se na resoluo dos litgios transfronteirios e organizar aces conjuntas de informao e esclarecimento aos consumidores dos dois pases. +Onde a cidade do homem, no do lobo mas irmo? + (...) Em 1975, nos Estados Unidos, a comisso trilateral divulgava um relatrio intitulado Crise da Democracia, no qual culpava pela contestao autoridade fundada na hierarquia, na competncia e no dinheiro ... intelectuais e grupos prximos, que estavam a tornar as democracias ocidentais, liberais, parlamentares, ingovernveis porque ameaadas por uma gravssima crise de autoridade, fomentada por intelectuais e jornalistas. +E conclua que S se pode sair desta crise com o restabelecimento frreo do princpio da autoridade. + De ento para c, o sistema tem vindo a domesticar esses sectores, seja atravs da seduo material seja pela atribuio de honrarias, pelo que tais sectores abandonaram a contestao s instituies. +Paralelamente, o controlo sobre os rgos de informao de tal ordem apertada que s vem a pblico a verdade oficial, a do Big Brother. +So trs os principais pontos litigiosos: a participao de organizaes opostas ao processo de paz, a possibilidade dos palestinianos de Jerusalm Oriental serem candidatos e eleitores, e ainda as competncias do Conselho Autnomo. +O chefe dos negociadores israelitas, Yoel Singer, realara anteriormente progressos em relao a uma srie de questes, salientando que as negociaes continuaro, dentro de duas semanas, e pela primeira vez em Jeric. +Na cimeira de hoje, em Erez, Arafat vai tentar convencer o primeiro-ministro israelita a fixar uma data para a retirada do Exrcito das cidades rabes da Cisjordnia e para a realizao de eleies, mas Rabin provavelmente hesitar uma vez mais, enquanto no obtiver garantias de segurana para os 130 mil colonos judeus. +O lder da OLP pretende tambm a abertura dos territrios, fechados desde 22 de Janeiro, para que 50 mil palestinianos possam trabalhar em Israel, embora queira tambm que Rabin explicite a sua ideia de uma separao total dos dois povos. +Arafat receia que a separao seja sinnimo de priso e no de independncia. +A vida de Albino Luciani, que seria o papa Joo Paulo I, encerra dois mistrios: as circunstncias da sua morte e o teor da sua conversa de duas horas com a irm Lcia, vidente de Ftima. +Depois desta conversa, Luciani nunca mais seria o mesmo, garante a sua famlia. + por isso que, explica, no tem pena de Hillary Clinton. +Eles [ Hillary e Bill Clinton ] podem ter alguma espcie de acordo e quem somos ns para dizer se bom ou mau? +Acho que eles tm mais maturidade em relao ao sexo, como em relao s drogas, do que podem mostrar. +Vivo num Estado de Ironia. +Campos faz parte destes rgos, bem como Manuel Alegre que cedo se colocou ao lado do eurodeputado em nome de solidariedades antigas. +A deciso de Guterres foi tomada depois de segunda-feira ter reunido o ncleo duro dos seus conselheiros polticos, tendo mantido outros contactos durante todo o dia. +Esta precipitao dos acontecimentos vem de encontro a algumas das crticas feitas actuao da Comisso Permanente, criticada por ter deixado arrastar durante semanas o contencioso com o eurodeputado, em vez de desde logo, como muitos defenderam, chamar Campos para este prestar esclarecimentos e colocar uma pedra sobre o assunto. +Na busca passada casa do suspeito, de 38 anos, que trabalha como marceneiro, foi encontrada a caadeira disparada trs dias antes contra a porta do clube, localizado naquele bairro, uma espingarda de presso de ar, diversos cartuchos e um panfleto de herona. + Apesar de haver suspeitas de o indivduo estar relacionado com o trfico de drogas duras nas Galinheiras e tambm na zona de Odivelas, onde possui outra casa, nenhuma quantidade significativa foi encontrada. +Elogiei a escolha do meu anfitrio, enquanto discorria, a despropsito, acerca do papel do consejero de cracion, que tinha talvez inventado aquela espcie de centro de mesa, com batatas acabadas de fritar (sim, batatas fritas), certamente em fatias espantosamente finas e em azeite a ferver, tratadas a modos de souffl. +Iniciei-me com uma morcilla de Burgos, enrolada numa massa fofa, e dediquei-me com carinho a um naco de bonito, frio e escabechado. +A conversa seguia animada, uma dose de marketing, 200 gramas de imagem, levadas ao lume com public affairs q.b. +Gosto de levar a srio o meu papel de consultor encartado. +Eduardo Loureno fechou, na sexta-feira, ao fim da tarde, os trabalhos da VI Reunio Internacional de Camonistas, que decorreu durante quatro dias na Universidade de Coimbra, com uma inspirada lio de encerramento em torno das ligaes entre Cames e Petrarca e da luz platnica que sobre ambos ter, ou no, incidido. + Anbal Pinto de Castro, organizador do encontro, obrigado pela funo a proferir algumas palavras finais, confessou o quanto lhe parecia sacrlego falar aps a interveno do recm-galardoado com o Prmio Cames. + de crer que estivesse a ser sincero. +Eduardo Loureno tem, de facto, esse dom, to contraditrio com a sua proverbial modstia, de dar a tudo quanto escreve um tom definitivo. +Provoca em quem o ouve a sensao de que aquilo que diz, o diz da forma mais justa, se no da nica forma justa. +Um talento que, por norma, cabe apenas aos poetas. +A France 3, uma das principais estaes pblicas da televiso francesa, abriu as hostilidades e declarou greve em nome do horrio laboral e da poltica salarial. +A France 2 segui-lhe os passos e as emisses dos dois canais foram interrompidas. +A direco j mostrou boa vontade, mas a greve prossegue em todas as delegaes do pas. + Concebidos para funcionarem como instrumentos privilegiados dos gestores de sistemas de informao, estes Compaq Proliant viram reforados os seus dispositivos de segurana e preveno de quebras no sistema -- reforo esse numa perspectiva de centralizao fsica deste tipo de equipamentos, sustentadores de redes cada vez mais complexas resultantes das evolues de downsizing e rightsizing a que muitos sistemas de informao estiveram ou viro a estar sujeitos. +Estes sistemas exigem um acompanhamento e controlo permanentes (as 24 horas de cada um dos dias da semana), pelo que a sua centralizao num nico espao permite racionalizar e conter os custos da sua gesto. +Dos dispositivos de segurana e preveno faz, evidentemente, parte o Insight Manager, aplicao de gesto j conhecida das linhas anteriores de servidores mas agora na sua verso 2.3. +A Orquestra Clssica do Porto abre hoje, em Guimares, a 4 edio dos Encontros da Primavera. +O primeiro concerto dirigido por Piero Bellugi, com os solistas Pedro Corostola (violoncelo), Maria do Rosrio Ferreira e Palmira Troufa (sopranos). + No Pao dos Duques de Bragana, s 21h45. +Final do Festria -- IX Festival Internacional de Cinema de Tria, com a exibio s 16h30 do filme premiado com o Golfinho de Ouro. +Um industrial do Porto, Manuel Magalhes, de 48 anos, abate a tiro dois scios, quatro disparos num, seis noutro, por terem passado cheques em seu nome. +Um casal de Aveleda, povoao do Minho, acusa o padre da aldeia, Joaquim Carneiro, de ter relaes sexuais com um filho seu de 12 anos. +O povo defende o sacerdote. +As autoridades arquivam o processo por falta de provas. +O arcebispado, porm, transfere o acusado para Paris. + necessrio que a construo [ da barragem do Ca ] seja efectivamente suspensa e que sejam reunidos os meios humanos e necessrios a um estudo completo de toda a rea antes que se crie uma situao irreversvel. + E no se invoquem os prejuzos imediatos de tal deciso, pois correspondero, certamente, a uma parcela diminuta dos encargos com essa obra faranica e desajustada que a Expo'98. +So afirmaes do reitor do Universidade do Porto, Alberto Amaral, no editorial do n 25 do Boletim daquela Universidade, inteiramente dedicado ao tratamento da grave situao do patrimnio histrico-cultural do vale do rio Ca, como igualmente refere o autor. +Comdia inglesa, anos 50. +Um friso inenarrvel de personagens marginais e/ou marginalizadas, com o ps-guerra em pano de fundo. +Pea nica e inimitvel no contexto do teatro ingls deste sculo, oscilando entre o naturalismo e o lirismo. +Receita que funciona lindamente, graas a um elenco talentoso e bem dirigido e a uma direco plstica (Jos Carlos Barros) eficaz. +TEATRO DA TRINDADE. +De 3 a sb., s 21h30; sb. e dom., s 16h. + Cumpridos 16 dias de uma greve de 45, a paralisao na Rdio Televiso da Guin-Bissau (RTGB) foi suspensa ontem at ao prximo dia 25, devido a uma cedncia dos trabalhadores que tm ouvido forte e feio da populao fora de Bissau que no tem acesso s emisses da RTPi -- especialmente por causa dos jogos da seleco portuguesa, acompanhados como se fosse a seleco guineense. +E foi assim que ontem -- precisamente por causa do Portugal-Turquia -- a televiso guineense reabriu ... hora necessria para transmitir o Portugal-Turquia, o que motivou grandes ajuntamentos em locais pblicos que dispunham de televisores, tanto em Bissau como no interior do pas. + A Lisnave no conseguiu atingir as metas fixadas pela sua administrao para o exerccio de 1994. +No final do primeiro semestre do ano passado, a empresa previa obter um volume de vendas de 24 milhes de contos, mas acabou por no ir alm dos 21 milhes. +No que se refere a resultados correntes, as estimativas apontavam para um valor negativo de 2,6 milhes de contos, mas que na realidade ultrapassou os seis milhes. + Ao nvel do Grupo, isto , a Lisnave propriamente dita e as restantes 21 empresas em que os Estaleiros Navais de Lisboa participam no capital, os resultados saldaram-se por um prejuzo de 16,2 milhes de contos. +Este valor reflecte um agravamento de 6,5 por cento face aos 15,2 milhes de contos de resultados negativos em 1993. +As vendas consolidadas foram de 26,7 milhes de contos em 1994, menos 1,2 milhes de contos do que no ano anterior. +Do ponto de vista concorrencial, o agravamento das perdas da empresa explica-se, em parte, devido entrada no mercado de novos estaleiros situados em pases de muito baixo custo de mo-de-obra (pases do ex-bloco de Leste), e a aumentos da capacidade de docagem em reas bem localizadas relativamente aos grandes fluxos de trfego martimo, alm de tambm disporem de mo-de-obra barata (pases do Mdio e Extremo Oriente). + O PRESIDENTE etope, Mengistu Hail Mariam, anunciou ontem a constituio de um novo Governo, de cuja formao ficou responsvel o chefe da diplomacia, Tesfaye Dinka, agora promovido ao cargo de primeiro-ministro., revelou a rdio de Addis Abeba. + A emissora anunciou igualmente que Mengistu demitira dois dos seus mais antigos ministros, o vice-Presidente Fiseha Desta e o secretrio do Comit Central, Legesse Asfaw. +Tesfaye, um economista formado nos Estados Unidos, tem a misso de renovar o Governo e de alargar a sua base tribal, nos termos de uma deciso adoptada pelo parlamento no incio da semana. + A PRXIMA ronda de negociaes sobre a paz em Moambique dever ter incio no dia 2 de Maio em Roma, confirmaram ontem o Ministrio italiano dos Negcios Estrangeiros e a comunidade religiosa de Sant'Egidio, mediador destas conversaes. +Em comunicados separados, as duas entidades deram conta de que os contactos preliminares principiaram ontem em Roma. +Segundo a agncia Lusa, a delegao da Renamoj se encontra na capital italiana, onde so esperados este fim-de-semana os negociadores de Maputo. + Apanhado pela ronda do PBLICO quando chegava quela praia acompanhado pela mulher e pelo filho, na tarde de quinta-feira passada, o ministro dos Negcios Estrangeiros portugus escusou-se a ser fotografado, com o argumento de estar em frias, acelerando o passo (mais tarde, quando abandonava a praia, j depois das 20 horas, acabaria por ser apanhado pela objectiva). +Conhecido pelos vizinhos banhistas, l foi acenando at se instalar mesmo junto gua. +No longe de Guilherme Oliveira Martins, o assessor poltico de Mrio Soares, que deixar Belm em Setembro para assumir por inteiro a sua condio de futuro deputado pelo Partido Socialista. +H muito frequentador do local (os pais possuem uma residncia em ...Boliqueime), Oliveira Martins tem visto os Tomates a transformarem-se na moda do Vero algarvio. +o filho de Marcelo Rebelo de Sousa fazia anos e o ex-comissrio de Lisboa 94 promovia uma festa, em local mais ou menos secreto. +Pela Praia dos Tomates, restava apenas um discreto centrista, Fausto Quadros, alm do porta-voz da Comisso Europeia, Joo Vale de Almeida. +O australiano Michael Doohan (Honda) fez ontem a desfeita aos adeptos italianos, ao vencer o Grande Prmio de Itlia de motociclismo em 500cc, quarta prova do Mundial da modalidade, disputada no circuito de Mugello, onde os espectadores puxavam fervorosamente por Max Biaggi (Honda), que no foi alm do segundo posto. +A Honda acabaria por fazer o pleno no pdio, com Alex Crivill a conseguir o terceiro posto. + Nos arredores de Ponta Delgada uma residncia do sculo XVII, inteiramente restaurada. + Quatro quartos, simples por 850000 e duplos por 980000; servem refeies quando solicitado e admitem animais. +A seduo de uns olhos de longas pestanas sedosas tem agora em Cil Sublime, da Bourjois, um precioso auxiliar. +Enriquecido com protenas e pr-vitamina B5, aplicado com uma escova especialmente criada para evitar o esborratamento do rmel. + N. R. A razo por que se realou no ttulo da notcia em causa o benefcio do whisky foi porque ela se refere a um estudo que realava esse facto, o que se compreende quando se sabe que se trata de um estudo britnico. +O PBLICO tem dedicado frequentes notcias aos benefcios do consumo moderado de lcool em geral e do vinho em particular. +Veja-se, a ttulo de exemplo, os textos Corao, vinho e legumes, PBLICO do passado dia 16; lcool faz bem ao corao, 19.11.93; Glrias e misrias do lcool, 15.8.93; lcool protege corao?, 3.9.91; Vinho contra colesterol?, 12.8.91. +O quadro de intenes levanta uma outra questo. + Na prtica, privilegia-se as intervenes de fachada em detrimento do bem-estar dos habitantes das aldeias. +A ilustr-lo o exemplo de Pido, onde as coberturas em telha das habitaes (mesmo estando em bom estado) esto a ser substitudas por outras em lousa, enquanto as casas que j tm coberturas em lousa (ainda que chova no interior) no beneficiam de qualquer ajuda. +Pessoalmente causa-me alguma perplexidade que se trabalhe para a fotografia do visitante e no tanto para o bem estar de quem l habita, confessou Jos Reis. +os restantes 25 por cento so participao nacional, sustentada pelos promotores dos projectos apresentados. +Mas tambm aqui h aspectos pouco claros, j que esta disposio no seguida por todos. +Quantos emigrantes muulmanos residem na Europa? +3 milhes residem em Frana, 1,7 milhes na Alemanha e 850 mil na Gr-Bretanha. + Na Holanda, Blgica, Itlia e Espanha, os nmeros oscilam entre 250 mil e 300 mil muulmanos. +A Jugoslvia tem 3 milhes, a Albnia 1,7 milhes e a Bulgria 800 mil. +O sindicato dos maquinistas tambm no contabilizou os custos desta greve. +No entanto, garante que os gastos com os autocarros fretados pela CP em Agosto para efectuar o transporte alternativo dos passageiros (principalmente emigrantes) dos comboios internacionais chegavam para resolver os seus problemas. +Fora 4, uma placa de mrmore disposta sobre um soclo, onde efeitos de ondulao criam a iluso da passagem do vento sobre a superfcie do mar. +Joo Cutileiro tem um peixe, que ironicamente se apropria de todas as formas de peixes / fontanrios de jardins, e que reenvia para a sua grande escultura de ninfas com um barco, mais longe, num dos lagos. +Jos Pedro Croft fez uma enorme caixa oca de pedra, dentro da qual disps bolas do mesmo material. + a nica pea que exige algum esforo ao visitante, que levado a debruar-se e espreitar para o interior da caixa. +Poder assim tomar conscincia do contraste entre o volume sugerido e a massa das bolas, e associar a caixa a outras formas semelhantes, feitas no mesmo material, que j tenha visto: tmulos, sarcfagos ... +Manuel Rosa e Rui Sanches, por fim, mostram-nos esculturas que tm por referente mais ou menos explcito o corpo. +mas a coluna ela prpria uma metfora do corpo que sustenta, da fora que tudo suporta. +e, no meio de uma estrutura de mrmore, colocou dois vasos com gua a diferentes nveis, que se foi sujando -- e enchendo de moedas -- com o passar dos dias. +A certa altura, este corpo, que o corpo da escultura, foi mesmo quebrado, levando sua substituio. +Esttua (porque um corpo), escultura, organismo que vive, ferido e morre? +Todas as interpretaes so possveis, tudo pode acontecer a este corpo. +BRASLIA Pesquisa Datafolha publicada hoje revela um dado supreendente: recusando uma postura radical, a esmagadora maioria (77%) dos eleitores quer o PT participando do Governo Fernando Henrique Cardoso. +Tem sentido -- alis, muitssimo sentido. +Muito mais do que nos tempos na ditadura, a solidez do PT est, agora, ameaada. +Nem Lula nem o partido ainda encontraram um discurso para se diferenciar. +Eles se dizem oposio, mas ainda no informaram o que vo combater. +Muitas das prioridades do novo governo coincidem com as prioridades do PT. +Desde o ltimo dia 13, Confisses de Adolescente pode ser vista pelos teens portugueses. +A srie exibida aqui pela Cultura estreou na TVI de Portugal. +Alm disso, a co-produo com o canal francs TCF1 para a realizao de mais 30 episdios continua sendo negociada. +Cmera Manchete o nome do novo programa jornalstico que estria quarta-feira, s 22h30, na Rede Manchete. +Sob o comando de Ronaldo Rosas, o programa mostrar reportagens especiais de Snia Pompeu. +A direo do novo semanal ser assinada por Ewaldo Ruy. +Os jogadores se dividem pelos dez quartos do alojamento, equipados com frigobar, ar condicionado, televiso e telefone. + uma coisa do Primeiro Mundo, afirmou o levantador Maurcio (leia matria ao lado). +Alm de Maurcio, Carlo e Paulo, a seleo deve contar hoje com Giovane. +O atacante, que deveria ter se apresentado anteontem noite, pediu mais um dia de folga ao treinador. +Na volta de uma viagem ao exterior, vale a pena trazer uma impressora matricial. +Free shops dos aeroportos internacionais tambm vendem o equipamento. +O modelo Lx 810, da Epson, vendido em Miami por US$ 178. +O preo de lista nas revendas brasileiras de US$ 422. +Esse equilbrio era tido como pr-condio para o sucesso do plano econmico. +Parte dos recursos para a formao do FSE foi deslocado do oramento da sade e educao. +Na poca, o ento ministro da Fazenda, Fernando Henrique Cardoso, fez um pronunciamento em cadeia nacional para anunciar a inteno do governo de destinar o FSE a investimentos sociais. +O assessor de imprensa do Ministrio da Fazenda, Srgio Danese, disse ontem que o ministro da Fazenda, Rubens Ricpero no iria comentar o assunto porque no tinha informaes suficientes. +O projeto original do governo destinava ao TSE R$ 334,9 milhes. +Como no houve acordo entre governo e tribunal quanto ao volume de recursos, a dotao foi includa na reserva de contingncia sem especificao de despesa. +Posteriormente, diante da ameaa do tribunal de entrar com uma ao judicial, o governo mandou ao Congresso uma alterao ao projeto, aumentando para R$ 452,7 milhes a dotao do TSE. +Essas medidas reduziram a disponibilidade de dinheiro no sistema bancrio e os grandes bancos passaram a no fornecer recursos para as pequenas instituies. +Quebraram aquelas que estavam descasadas, ou seja, que financiavam no mercado interbancrio de um dia (CDI) os emprstimos e carteiras de ttulos estaduais e municipais com prazos longos. +Tel -- Claro. +Aqui s joga quem est bem. +Ningum fora sua escalao porque h quem escale o time no So Paulo. +Ele s no jogava porque no estava bem. +Folha -- E o que o senhor acha do escndalo da arbitragem carioca? +Tel -- Uma vergonha. +Apenas dois rbitros resolveram contar todos os podres, enquanto a federao tem mais de 70. +O futebol precisa seguir o exemplo da CPI do oramento e apresentar todos os podres. +Se eu dirigisse uma federao, apresentaria balanos mensais e liberaria minhas contas bancrias. +A Fifa e a CBF deveriam entrar de sola nesse caso e em todas as outras federaes. +A maioria das empresas que produzem leite das marcas interditadas no tinha sido comunicada ontem sobre a liberao do produto. +O presidente da Cooper, Benedito Vieira Pereira, 49, afirmou que pretendia distribuir leite C nos postos de venda hoje. +Prandi disse ainda que a empresa est elaborando normas factveis de serem executadas para a soluo ou minimizao dos problemas existentes no local. +O Charade vai concorrer na faixa do Suzuki Swift e do Twingo, da Renault, afirma Caparelli. +Herbert Berger, diretor-superintendente da empresa, diz que o Charade se aproxima do Honda Civic em tamanho e custa bem menos. +O Applause, um sed quatro portas, com motor 1.6, o carro mais caro da Daihatsu. +O top de linha custa US$ 30 mil. +A mudana do local de jogo que deve acontecer tambm na partida contra o Corinthians, no prximo dia 17 foi determinada pela CBF, que no viu garantias de segurana no estdio santista. +Na Vila, quando recebo a bola, tenho que ficar olhando sua trajetria, para no ser surpreendido. +S depois que levanto a cabea para fazer um lanamento, reclama Neto. +JFK -- A PERGUNTA QUE NO QUER CALAR Telecine, 20h30. +O destaque do HBO o indito Exterminador do Futuro 2 -- O Julgamento Final, em que Schwarzenegger um rob com aparncia humana que vem do futuro para proteger um garoto. +Para o terceiro ru, Alexandre Cardoso, 21, o Topeira, o juiz determinou uma pena de 20 anos. +Souza tambm negou aos rus o direito de apelarem da setena em liberdade. +Os trs esto presos desde 30 de julho de 93. +Comeou bem antes do que se previa a batalha pela futura sucesso na Fifa apenas seis meses depois do super-acordo que, nas vsperas da Copa, reconduziu o brasileiro Joo Havelange ao sexto mandato consecutivo. +Pelo acordo, os trs continentes mais obstinados em cortar o reinado de Havelange, a frica, a sia e a Europa, aceitaram cancelar os seus movimentos de oposio em troca, basicamente, de dois compromissos. +Havelange aceitaria engolir o italiano Antonio Matarrese como o seu vice-executivo e, alm disso, esqueceria os seus modos autoritrios, coordenando a entidade de maneira colegiada. +O Ambulim foi um dos centros que contriburam para um estudo apresentado na 5 Conferncia Internacional sobre Transtornos Alimentares, de 29 de abril a 1 de maio em Nova York. +Dados sobre abuso sexual em bulmicas no Brasil, ustria e Estados Unidos foram centralizados por Harrison Pope, da Escola de Medicina de Harvard. +Essa diviso gera algumas distores terrveis. +Um dos injustiados Alfredo Volpi, que recebe apenas um painel, com cinco telas que servem para ilustrar sua evoluo de figurativo a abstrato. +Afora historicismo, isso menosprezar um fator interno arte brasileira, que independe de contexto internacional. +Volpi foi dos mais influentes pintores do pas para alm da questo da autonomia. +O panorama sofre prejuzos demais em favor da tese. +Abstratos entre medianos e medocres, como Fukushima, Prsio, Raimo e Douchez, tm o mesmo ou maior destaque que Volpi e nada que se possa chamar de autonomia para oferecer como lenitivo. +Talvez isto seja muito barulho por nada. +Original: Ccero. +Disse que no conseguia vislumbrar artifcios fraudulentos ou prtica de peculato no protocolo assinado por Qurcia. +Afirmou que o conjunto de fatos, em princpio, aponta o envolvimento de Qurcia. +Recebeu a denncia. +Segundo o mdico, o caso no preocupa. +Romrio no se exercitou nas cobranas de falta e pnalti. +Toda a comisso tcnica sabe que Romrio de treinar pouco, geralmente se poupando entre dois jogos difceis. +Nem o PSB nem a coligao tm competncia legal para trocar o vice da chapa sem a concordncia de Bisol, que teve o nome aprovado em conveno. +Outra maneira de um partido forar a substituio seria expulsar o candidato, com base em seu estatuto. +Neste caso, o registro da candidatura seria cancelado pela Justia. +Uma boa parte do pblico dos Stones hoje yuppie. + uma tendncia, dizia o lojista Nivaldo Silva Costenaro, 34, cabeludo baterista de uma banda de blues. +Escuto Stones desde os 13 anos de idade. +O f daquela poca vai ser f sempre, acrescentou Costenaro. +Apesar de limitar a venda de quatro ingressos por pessoa, a Mesbla no evitava ontem que uma mesma pessoa comprasse mais de uma vez. +A queda nas vendas teve reflexos nas negociaes entre as confeces e as lojas. +No est havendo cancelamento de pedidos. +Mas existem indstrias que esto sendo procuradas pelos lojistas para postergar as entregas, conta Eduardo Costa, diretor da Abravest, que rene a indstria de vesturio. +A seca que atingiu as reas produtoras de gros no deve causar grandes estragos na safra 1994/95. +A primeira previso do IBGE (Fundao Instituto Brasileiro de Geografia e Estatstica) indica queda de 0,62% na rea plantada nesta safra em relao a anterior. +Pocock, que publicou os resultados no ltimo nmero da revista British Medical Journal, do Reino Unido, estudou a influncia do chumbo desde a gestao. +Segundo ele, a exposio ao material durante a gravidez ou nos dois primeiros anos de vida no representa perigo. +OS PAPIS DE FLAVIO-SHIR -- A mostra, que faz parte das comemoraes dos 50 anos de pintura do artista japons, rene 25 obras em pequenos formatos. +Foi utilizada tcnica mista, incluindo desenho, pastel, ceragrafia e fotografismo. +De seg a sex das 11h s 19h, sb das 11h s 14h. +At 30 de maro. +Preo das obras: de US$ 2.000 a US$ 4.000. +A aplicao dessas observaes ao caso americano e s relaes entre negros e brancos sugere uma nova maneira de conceituar os argumentos j conhecidos do legado da escravido. +No se trata simplesmente da questo da escravido certamente ter tido efeitos duradouros sobre a cultura negra, nem mesmo dela ter exercido um amplo efeito negativo sobre a auto-confiana e auto-estima dos negros, mas mais especificamente da experincia da escravatura ter desvirtuado e tolhido a evoluo do algoritmo etnocntrico que os negros americanos teriam desenvolvido no decorrer normal dos acontecimentos. +Os brancos fizeram tudo em seu poder para invalidar ou menosprezar cada sinal de talento, virtude ou superioridade entre os negros. +Eles tiveram que fazer isso se os escravos fossem superiores em qualidades que os prprios brancos valorizavam, onde estaria a justificativa moral para mant-los escravizados? +E, assim, tudo o que os afro-americanos faziam bem teve de ser colocado em termos que menosprezassem a qualidade em questo. +Mesmo a simples tentativa de se documentar esse ponto deixa uma pessoa exposta a acusaes de condescendncia e, assim, os brancos de fato conseguiram cooptar os julgamentos de valor. + ainda mais bvio que impossvel falar abertamente sobre o superioridade de muitos atletas negros sem ser sujeito a acusaes de que se estar sendo anti-negro de uma maneira enviesada. +Pela segunda vez desde quando comeou a coordenar as aes no Rio, h duas semanas, o Exrcito mudou o nome das operaes. +Agora, os oficiais envolvidos se referem sempre ao comando das aes como Centro de Coordenao de Operaes de Combate ao Crime Organizado (Ccocco). +Cinco linhas paralelas, de mais de 400 km cada, foram descobertas por cientistas australianos no sul do pas. +Elas esto separadas por espaos de 80 km a 100 km. +As linhas, invisveis da superfcie, foram detectadas atravs de dados de satlites. +Pesquisadores acham que as linhas podem ser falhas geolgicas. +O derramamento de leo do petroleiro Exxon Vldez, em maro de 1989, causou estragos no valor de US$ 286,8 milhes, segundo um jri em Anchorage (Alaska). +A ao est sendo movida por pescadores, lojistas, propietrios de terra e nativos. +O valor mais que o dobro do estimado pela Exxon, mas menor que o original pedido, de US$ 895 milhes. +Bombeiros e pessoal de resgate foram colocados em alerta mximo em Argel no final da tarde de ontem, segundo a rdio estatal. +Veculos de resgate estavam a apenas 500 metros do Aibus 300. +Optar entre um aparelho conjugado e outro simples outro ponto que merece ateno. +Para fazer uma boa compra, a tcnica do Procon recomenda ao consumidor que verifique se j h dentro de casa os tradicionais equipamentos que desempenham as mesmas funes do conjugado, s que separadamente. +Caso a opo seja pelo aparelho multiuso, o comprador deve checar se o produto tem assistncia tcnica, diz ela. +Como a idia de enxugar a Constituio enfrenta resistncia inclusive nos partidos que apiam o governo, a equipe de FHC resolveu fazer as reformas por partes. +Primeiro aprova-se o texto enxuto e depois negocia-se a aprovao, sem prazo definido, das leis complementares e ordinrias. +O Pentgono usa a Internet, que conecta computadores a sistemas telefnicos, para que seus funcionrios troquem informaes. +Os arquivos so protegidos por senhas e cdigos, que acabaram mostrando-se vulnerveis. +O conselho fiscal entende que os acionistas no devem mais suportar investir dinheiro bom em companhia sem condies de se reerguer. +Tem que torcer muito e rezar bastante, porque milagres acontecem. +QUADRINHISTAS DO BRASIL inteiro se renem entre sexta-feira e domingo em Arax (MG). +Haver oficinas e um concurso. +Informaes pelo telefone (034) 661-2458. +O MERCADO AMERICANO para desenhistas o tema da oficina com David Campitti (veja trabalho dele ao lado), roteirista de histrias do Super-Homem e criador de vrios personagens. +Outros profissionais brasileiros, que atuam nos EUA, tambm participam. +Informaes pelo (011) 263-4700. +Ontem pela manh, os atletas que no atuaram contra o Vitria, participaram de um coletivo com a equipe principal. +Os que jogaram, fariam treino de recuperao tarde no Projeto Acqua. +Murici reclamou da postura do time do Vitria. +Eles bateram muito. +O que fizeram com o Pereira foi um absurdo, disse. +Pereira levou um piso na cabea, mesmo estando cado no cho. +Murici espera que o Inter no tenha a mesma atitude. +A primeira misso lunar dos EUA em 21 anos teve incio ontem, quando um foguete Tit 2 colocou no espao a astronave no tripulada Clementine 1, que vai passar dois meses em duas rbitas da Lua para realizar completo mapeamento mineralgico e topogrfico do satlite da Terra. +O nome oficial do projeto Depse 1 (Deep Space Program Science Experiment). +Ele patrocinado pela Organizao de Defesa de Msseis Balsticos e pela Nasa, em uma das primeiras operaes espacais com fins civis e militares. +O lanamento ocorreu ontem na base area de Vanderberg, na Califrnia, costa oeste do pas, s 8h30 locais (14h30 em Braslia). +O custo total do projeto de U$ 55 milhes. +O atual perfil dos poupadores ajudar a manter o dinheiro aplicado. +Segundo o BC, mais da metade dos recursos da caderneta so de poupadores de mdio e grande porte em tese, menos sujeitos a achar que o dinheiro perdeu rendimento com a queda da inflao. +A previso de saques reduzidos sobre a poupana compartilhada por especialistas. +O ceticismo em relao a planos econmicos faz a populao manter reservas, diz o ex-diretor de Poltica Monetria do BC, Lus Eduardo de Assis, diretor de investimentos do Citibank. +s vezes peco por falta de experincia, mas me considero um piloto rpido, afirmou ontem o brasileiro, em entrevista Folha por telefone, da Itlia. +Diniz comeou sua carreira automobilstica em 1989, no Brasileiro de Frmula Ford, campeonato em que obteve a sexta posio na classificao final. +A Polcia Civil de Ourinhos (371 km a oeste de So Paulo) prendeu ontem tarde o ex-lder religioso Jonas Rbio, 45, acusado de matar na quarta-feira a estudante Claudirene Contijo, 13, com um tiro de espingarda. +O delegado Celso Antonio Borlina, 38, disse que Rbio confessou o crime. +Rbio era acusado por Claudirene de t-la estuprado no ano passado, poca em que era o lder da Assemblia de Deus na usina So Luiz, onde a estudante morava. +A me da estudante prestou queixa do estupro e, depois, das ameaas de morte que Rbio teria feito a Claudirene, disse Borlina. +Zanini diz que est preparando a fundao de um novo partido, que trar explcito em seu programa a ressalva de que contra o racismo. +Ser um movimento nacionalista independente. +O novo manifesto do movimento no ficar pronto antes das eleies. +Sendo assim, no podemos dar nosso apoio aberto, explicou. +Ele afirma ter negros e judeus entre seus correligionrios. +Segundo a empresa, ser o primeiro alimento produzido totalmente atravs de tcnicas de engenharia gentica a ser vendido. +O tomate foi projetado para manter o sabor que tem depois de colhido durante tempo prolongado na prateleira dos supermercados. +Pases produtores e consumidores de caf entraram em um tipo de pr-pacto internacional que no intervenha nos preos do produto, afirmou o porta-voz da Organizao Internacional do Caf (OIC). +Temos as bases para um novo acordo. +Pode-se agora recomendar um texto definitivo para o conselho da organizao, disse o porta-voz. +Os pases integrantes do instituto enviam o texto com o novo acordo para seus governos antes da aprovao final, que deve ocorrer na semana que vem. +Passarinho j descarta claramente a candidatura ao Planalto. +Meu compromisso com o Par inarredvel, afirmou ontem depois de conversar com Amin. +O senador catarinense ainda hesita. +Mas faz questo de lembrar que, aos 46 anos, ainda ter vrias outras oportunidades para disputar a presidncia. +A maior facilidade criada a supresso das barreiras alfandegrias para a importao de produtos de um pas pelo outro. +Isso vai facilitar as atividades de franqueadores que precisam importar produtos para distribui-los a seus franqueados que operam em outro deles. +Por exemplo: o master-franqueado da Arby's no Mxico enfrentar menos problemas e menos burocracia para importar o rosbife (que, ao contrrio do que ocorre com a Arby's Brasil, vem dos EUA) e as batatinhas (canadenses) que serve em seus restaurantes prprios e franqueados. +O maior impacto do Nafta a curto prazo ser sobre as franquias do setor automotivo. +Pelo acordo, so reduzidas de imediato todas as barreiras tributrias e alfandegrias relacionadas importao por qualquer pas do acordo, de veculos automotores, autopeas e acessrios fabricados nos demais pases membros. +Roberto Capuano, presidente do Creci-SP (Conselho Regional de Corretores de Imveis), diz que o mercado de usados sofre com a carncia de financiamentos desde 70. +Em 79 houve uma leve reabertura de crdito, mas nada significativo, afirma. +Segundo ele, hoje as concesses de financiamento bancrio so restritas e elitistas. +A Benetton volta a provocar polmica com um cartaz da srie United Collors. +Desta vez, com uma camiseta branca furada a bala e empapada de sangue acima de calas militares. +A empresa informou que a roupa era de Marinko Gagro, soldado croata-bsnio morto em julho do ano passado. +L'Osservatore Romano, o jornal do Vaticano, qualificou a campanha de US$ 15 milhes como terrorismo de imagem. +Os franceses Le Monde e Le Figaro e o alemo Frankfurter Allgemeine recusaram o anncio. +Ilmar Galvo e Moreira Alves votaram pela concesso do mandato de segurana. +Eles sustentaram que a renncia interrompeu o processo de impeachment. +Moreira Alves tambm votou a favor de Collor em outros dois mandatos de segurana, mas foi vencido nos dois julgamentos. +Aquele pensamento provocou-me um arrepio estranho e delicioso. +No falei nada, mas visualizei a Sra. Oke, sentada no salo amarelo, o mesmo salo onde nenhum Oke de Okehurst exceto ela ousava permanecer sozinho, envergando o vestido de sua antepassada e confrontando-se, por assim dizer, com aquela coisa vaga, plangente, que parecia permear o aposento ... aquela vaga presena -- assim me parecia do galante poeta assassinado. +O filme Barfly (traduzido no Brasil como Condenados pelo Vcio) foi inspirado na vida de Bukowski. +O escritor, identificado com a cultura beatnik, admitia somente ter sido inspirado por um outro autor da Califrnia, John Fante (Pergunte ao P, entre outros), que Bukowski descobriu nas suas leituras em bibliotecas pblicas quando jovem. +Michael Schumacher culpou um degrau existente nas zebras do circuito de Adelaide por seu acidente ontem no final da primeira sesso de treinos oficiais. +Quando analisei a pista na quinta-feira, pedi FIA que melhorasse as condies daquele ponto, disse o alemo. +Na sua opinio, o Estado no pode continuar em setores onde a iniciativa privada j comprovou sua eficincia. +Em entrevista Folha, Angarita elogiou a iniciativa do Sindicato dos Eletricitrios de So Paulo, que articulou uma ao judicial para tentar suspender a venda de aes da Eletropaulo. +Cutler admitiu que as conversas entre funcionrios da Casa Branca e do Tesouro sobre investigaes sobre o banco de McDougal no eram recomendveis, mas no constituram crime. +Essas conversas levaram em maro demisso de Bernard Nussbaum, amigo da primeira-dama, do cargo de assessor jurdico da Casa Branca. +Fiske comeou seu trabalho em 20 de janeiro. +Comisso Parlamentar de Inqurito sobre o caso Whitewater comea a funcionar dia 29. +O relatrio no diz se as reunies entre funcionrios da Casa Branca e do Tesouro violaram a tica. +O secretrio do Tesouro, Lloyd Bentsen, disse que a questo tica vai ser examinada agora. +S o procurou tempos depois, quando o secretrio de Assuntos Especiais, Eliezer Baptista, resolveu preparar o macroplanejamento do pas uma idia esplendorosa que, infelizmente, no resistiu ao impeachment de Collor e convocou Mouro para sua equipe. +Com medo de perder poderes, Modiano chamou Mouro e, numa atitude mesquinha, comunicou-lhe que, como o financiamento do projeto seria bancado pelo BNDES, ele iria para l, mas na qualidade de representante do banco. +A tela de abertura do programa simptica. +Uma sala tem como cones uma pilha de discos, um aparelho de som, uma enciclopdia e uma janela que mostra a encruzilhada onde Robert Johnson teria feito um pacto com o diabo, trocando a alma pelo sucesso. +Peppermint Tea House -- The Best of Shoukichi Kina. +Lanado dentro do projeto Asia Classics, um subselo do Luaka Bop (o mesmo que lanou Tom Z no exterior), essa coletnea mais um passo de Byrne na sua busca do sublime na msica. +No primeiro volume de Esses Byrne mostrou soberbas trilhas sonoras de filmes indianos. +Com Kina, ele chegou bem perto da perfeio. +Em todo o shopping, que est decorado com lanternas, leques e at um portal japons, h exposies de iquebana (arranjos florais), bonecas, cermicas, armaduras, mscaras, quimonos e espadas. +Em outro espao, h exibio de vdeos que demonstram, por exemplo, a cerimnia do ch e o teatro kabuki, alm de pontos tursticos do Japo. +Prximo praa da alimentao, um sushiman estar preparando pratos tpicos, como sashimis e o sushis, que estaro venda. +Para isso, ele afirma pretender inaugurar uma cmara setorial para discutir as questes especficas da economia no Estado. +Para atender poltica desejada pelo governador eleito, Barelli vai comandar uma reviso da antiga pasta (Secretaria das Relaes do Trabalho). +O economista diz que, para gerao de empregos, vrias propostas tero que ser analisadas. +Segundo ele, nem os grandes pases souberam resolver o problema do desemprego. +Segundo ele, italianos e belgas j descobriram que comprar imveis em Natal um timo negcio. +Um diplomata da Blgica comprou apartamento para passar as frias de julho e janeiro, mas fora desse perodo ele aluga o imvel por US$ 1.000, diz Bezerra. +Apesar de morarem aqui do lado em Miami, Teresa e Emerson Fittipaldi no do nem tchuns para a Copa. +Pegam os dois pimpolhos Joana e Luca e se mandam para Lake Powell, Arizona. +Para tanto imprescindvel priorizar algumas aes como a recuperao de unidades armazenadoras de cereais e das rodovias, alm do incentivo utilizao das ferrovias. +Tambm necessrio alertar toda a sociedade para a importncia de se reduzir as perdas de alimentos. +Aos horticultores preciso oferecer informaes de mercado seguras e atuais para a programao da produo e desenvolver novas alternativas para o acondicionamento e embalagem dos produtos. +Helmet brutal. + metal punk. +Tem tamanho, peso intrnseco que dispensa volume alto. +A banda foi cortejada por vrios selos multinacionais, que viam nela um possvel novo Nirvana. +Acabou assinando com o Interscope, ligado Warner, por US$ 1 milho por trs discos. +Como notou o jornalista Jim Greer, da revista americana Spin, uma boa grana, mas nada muito absurdo. +Meantime, disco de estria da banda no Interscope, vendeu 500 mil cpias. +As duas guitarras do Helmet atacam sempre juntas, em pequenas clulas separadas por um silncio nunca menos que perturbador. +Algo como um samba de breque transposto para o domnio eletrificado do rock. +As letras irnicas, eruditas, denunciando que os msicos talvez tenham estudado um pouco demais so gritadas. +No h nenhuma nfase na melodia. +O Helmet uma grande seo rtmica, definiu o baixista da banda, Henry Bogdan, em uma entrevista Spin. +Mtodo japons exige calhamaos de exerccios; opositores dizem que 'tcnica peca pelo mecanicismo'. +A matemtica ainda o bicho de sete cabeas para grande parte dos estudantes do 1. e 2. graus. +Na semana passada, a Secretaria Nacional de Direito Econmico baixou uma portaria, que aplica uma multa de 250 Ufirs (R$ 170) aos seis colgios por cada matrcula recusada. +A Justia foi aplicada porque o Estatuto da Criana e do Adolescente garante o direito de escolas a todas as crianas, disse Mrcia Mendes, presidente da Associaos dos Pais de Alunos do Pitgoras, um dos seis colgios acusados de recusar as matrculas. +O mesmo no se pode dizer da pintura de Cy Twombly. +Da segunda gerao de expressionistas abstratos, Twombly tambm do segundo time. + um grande pintor, e sua ltima fase o que mais sustenta a afirmao; mas irregular, tem fracassos evidentes. +Ainda que Robert Hughes, da revista Time, ache que o tema de Twombly mesmo o fracasso, um fracasso sempre um fracasso. +Os brasileiros escolhem Cardoso foi o ttulo na primeira pgina. +O jornal compara a eleio presidencial brasileira francesa, afirmando que o premi Edouard Balladur, ex-favorito, tambm pode perder, em abril de 95. +A explicao da reviravolta no reside em uma pretensa especificidade brasileira, que seria um coquetel de leviandade, inconstncia e pusilanimidade, tudo ao som de salsa, ironiza o editorial. +Os empresrios abriram mo de posies histricas, eventualmente visando sua proteo, para construir e defender idias exclusivamente de interesse coletivo para o desenvolvimento global. +Folha -- Quais so as implicaes que o encerramento da reviso traz para a economia? +Resposta -- J pedimos a criao de um conselho para produzir essa definio. + medida que o grupo de servios que entram nos lares e nas empresas evolui, a definio de servio universal vai evoluir tambm. +Deve haver, no mnimo, uma linha digital chegando a cada casa, mas acho que servio universal deveria significar muito mais do que isso, mais do que o fio. +Amato -- O cenrio ideal para o Plano Real seria a vitria inequvoca de um dos candidatos j no 1 turno das eleies. +Folha -- Quem vence? +Eu tenho uma origem poltica. +Meu av materno foi prefeito de Limeira, meu pai foi duas vezes prefeito de Itapira, onde passei toda minha infncia. +Desde cedo fui orador da turma, sempre gostei de declamar. +Da a alguns anos, os Gershwins foram para Hollywood, a fim de fazer o que intimamente desprezavam: escrever canes para filmes. +Mas, mesmo que quisessem no conseguiriam fazer nada menor. +Num nico ano, 1937, eles compuseram They Can't Take That Away From Me, Let's Call the Whole Thing Off, A Foggy Day, Nice Work if You Can Get it, They All Laughed, Love Walked In' e Love Is Here to Stay', e essas so apenas as que ficaram universalmente conhecidas. +Infelizmente, 1937 seria tambm o ltimo ano de George, porque ele morreria em julho, de um tumor cerebral, aos 38 anos. +A desgraa que, cientificamente, como demonstra a moderna criminologia, quase nada dessa poltica criminal simblica serve para atenuar o gravssimo problema da criminalidade. +A interveno militar localizada, que pode imediatamente oferecer algum alvio, tem como efeito principal o seguinte: o crime s muda de lugar. +Com a interveno no Rio, que se cuide o resto do pas! +Quarenta por cento dos gerentes japoneses esto insatisfeitos com os salrios recebidos, segundo pesquisa divulgada por um instituto privado em Tquio. +Cerca de 72 % dos empresrios da construo querem que o prprio setor negocie a converso dos contratos para a URV, enquanto 28 % desejam que o governo estabelea as regras. +Folha Qual a sada? +Broz No tenho a receita exata, tenho a certeza de que algo precisa ser feito diante do confronto entre nacionalismo albans e srvio. +O candidato deu rpida entrevista no saguo do aeroporto e dirigiu-se ao Hotel Delmonicos, na Park Avenue, em Manhattan, onde a diria de US$ 195 (CR$ 253 mil) no fim de semana e US$ 250 (CR$ 325 mil) de segunda a sexta-feira. +Lula e outros 20 convidados almoaram a convite do cnsul-geral do Brasil em NY, Marco Cesar Meira Naslausky, no restaurante Peter Luger Steak House, tradicional no bairro do Brooklin. +A conta, paga pelo embaixador, ultrapassou US$ 1,4 mil (CR$ 1,8 milho). +A lio parece clara em ambos os casos: no ser um artifcio legal nem a boa vontade dos governos que protegero a riqueza dos poupadores ou o poder de compra dos assalariados, mas sim um ambiente de estabilidade econmica consistente. +Pelas razes alinhadas, que no refletem apenas conjeturas tericas, mas a experincia duramente vivida pela nao nas ltimas duas dcadas, se impe dar um basta definitivo a todos os remanescentes de indexao formal, sejam eles nas taxas de juros nominais (TR), no recolhimento dos tributos (Ufir e correlatos) e nos salrios (IPC-r). +O castelo de cartas ruiu logo. +H uma esquerda reformista no PSDB. +Mas ela parece ser mera exceo. +Agora, o partido retirou a mscara. +Mostrou que a maioria dos seus polticos profissionais cuja respeitabilidade no pode ser posta em dvida oscilava entre o centro nacionalista e os radicais do PMDB. +Considerados globalmente, no faziam diferena. +Eram o retrato do corao e do crebro deste partido. +Nada mais! +O tancredismo concedeu ao seu principal terico, professor e senador Fernando Henrique Cardoso, a oportunidade de esboar a filosofia poltica do mudancismo. +Na prtica, os que iriam constituir o cerne e a base do partido iam mais longe: condenavam o autoritarismo, o oportunismo, o clientelismo e o fisiologismo. +Ostentavam um patamar aparentemente slido, que evidenciava a radicalidade dos estratos mdios e dos intelectuais envolvidos na rejeio do status quo. +Na largada, Ayrton Senna, que havia feito a pole position, manteve a primeira colocao. +Por causa de um acidente envolvendo o finlands J.J. Letho, da Benetton, e o portugus Pedro Lamy, da Lotus, o safety car entrou na pista. +Durante seis voltas, os pilotos tiveram que manter suas posies por razes de segurana e aproveitavam para aquecer os pneus enquanto os carros envolvidos no acidente eram retirados. +O caso Paubrasil j rendeu Receita Federal, segundo o coordenador-geral de fiscalizao do rgo, Luiz Henrique Barros de Arruda, cerca de US$ 1,5 milho em crdito tributrio -- a soma do imposto devido, multas, correo monetria e juros de mora -- de cerca de 20 empresas envolvidas no escndalo que procuraram a Receita espontaneamente. +Essas empresas, diz Arruda, reconheceram ter emitido notas frias, sem o recebimento de nenhum servio da empresa do pianista Joo Carlos Martins e de seu scio Ettore Gagliardi. +Os jogadores do Palmeiras treinam s 9h, no Centro de Treinamento. +O treino deve ser s recreativo. +Depois do treino, os jogadores voltam ao Lord Hotel, onde esto concentrados desde a noite de ontem. +Interessado em organizar um livro de arte sobre viajantes, Ronaldo Graa Couto, da produtora Metavdeo/Metalivros, tomou conhecimento da pesquisa e procurou Belluzzo. +Couto procurou a Odebrecht. +Entre 40 outras idias, Brasil dos Viajantes foi selecionado para comemorar os 75 anos de atividade do grupo empresarial. +Como naquela poca ainda no existia a fotografia, eram os pintores que faziam os retratos das famlias. +Rembrandt era o preferido das pessoas mais importantes. +Ele era to bom artista, que muitos pintores quiseram aprender a pintar com ele. +O livro Apresentando Rembrandt -- Pintor, Gravador, Professor (editora Klick), de Alexander Sturgis, conta muitas outras coisas sobre a vida e as obras do pintor. +O governo do Estado realizar um concurso pblico para preencher 200 vagas de agente fiscal. +As inscries comeam no prximo dia 12, nas agncias dos Correios. +A taxa de CR$ 4.300,00. +Cerca de 20 mil pessoas fizeram cursos proficionalizantes no Senac (Servio Nacional de Aprendizagem Comercial) no ano passado, na Paraba. +Segundo o diretor regional da entidade, Glauco Pereira Chaves, a crise no mercado de trabalho estaria dificultando o aproveitamento dos formandos. +Tudo indica que os telejornais da Globo vo ganhar novo reprter especial. +Antes de estrear como editor senior da Playboy, Nirlando Beiro fez uma srie de testes de vdeo. +Cientistas dos EUA confirmaram ontem em Quito (Equador) a contaminao causada por atividades petrolferas na Amaznia equatoriana. +Segundo eles, casos de doenas de pele em ndios estariam relacionados contaminao. +O Centro para Direitos Econmicos e Sociais e mdicos da Universidade Harvard informaram que amostras de gua tm nveis entre 10 e 1.000 vezes maiores que os recomendados pela Agncia de Proteo Ambiental dos EUA. +Para tentar resolver o problema do trfico de drogas e da violncia no Rio de Janeiro, o governador do Rio e o Presidente da Repblica chamaram o Exrcito. +Agora, os soldados e a polcia esto trabalhando juntos para prender os traficantes. +Eles esto fazendo uma guerra contra o trfico. +S quando h problemas muito graves, como o do Rio, o Exrcito chamado para ajudar. +Quando Jos Sarney comeou a escrever semanalmente para a Folha, chegaram protestos redao. +Como aceitar que, depois de uma passagem to ruim pela Presidncia, Sarney dispusesse de espao privilegiado no jornal? +O tom familiar, coloquial, benigno de suas crnicas (agora reunidas em Sexta-feira, Folha, ed. Siciliano) foi pouco a pouco vencendo as resistncias do pblico. +Como cronista, Sarney toma o cuidado de no indignar ningum, e difcil antipatizar com ele. +Como a separao das marcas tida como uma tendncia irreversvel, caberia Volkswagen construir sua prpria fbrica. +A TV-A atribui ao Plano Real a conquista de 17 mil assinantes em outubro, contra a mdia de 10 mil assinaturas/ms no incio do ano. +Segundo o ministro, os juros desses emprstimos sero compatveis com os praticados no mercado internacional (8 % a 12 % ao ano). +Ciro no quis confirmar se a TR (Taxa Referencial) deixar de corrigir esses financiamentos. +Ciro minimizou a estimativa de inflao de 3% em outubro, de acordo com a Fipe. +A prpria Fipe prev que em novembro isso tudo passa e a inflao cai, disse. +Na segunda seguinte, o dia 4 ao dia 8, os bancos ficaro abertos at uma hora aps o expediente normal, apenas dez delegacias regionais do Banco Central e em agncias do Banco do Brasil encarregadas de entregar o real aos demais bancos. +A distribuio acontecer at o final do ms, partindo das capitais para o interior do pas. +Cada banco buscar no BC ou no BB o volume de dinheiro compatvel com suas operaes. +Apenas 5 milhes de contribuintes haviam entregue sua declarao de Imposto de Renda at ontem pela manh. +A estimativa da Receita Federal era receber 7 milhes de declaraes. +O nmero final do total de declaraes entregues s deve ser divulgado na prxima semana. +Os 12 computadores da Future Kids oferecem jogos educativos que treinam o raciocnio lgico. +No estande da Tec Toy h trs consoles Mega Drive, alm de um Master System e quinze cartuchos de jogos. +As crianas podem ficar 15 minutos em cada um dos jogos. +Caso no haja fila, o perodo de uso pode ser maior. +A partir do dia 17, a Playtronic tambm ter um estande no Iguatemi. +Quatro meses depois do atentado, o governo argentino ainda no conseguiu avanar nas investigaes sobre o atentado. +No incio do ms, o presidente Carlos Menem criou um fundo especial de US$ 1 milho para recompensar quem fornecesse informaes que ajudem nas investigaes. +Quando os britnicos retomaram o Egito em 1801 a pedra foi parar no Museu Britnico, mas os franceses fizeram cpias dela. +O resultado foi que um francs, Jean-Franois Champollion, que s visitou o Egito em 1828, foi o primeiro a decifrar os hieroglifos. +Uma carta de Champollion de 1822 Academia de Inscries e Belas Artes, mostrando seus resultados, o nascimento da egiptologia, segundo Vercoutter. +Hoje, a praia ganhou nova infra-estrutura com bares, quiosques, sistema de iluminao, banheiros e quadras de esportes. +Os jovens tm se encontrado em Atalaia, principalmente entre quarta-feira e domingo, quando so realizados jogos de voleibol. +A verdade simples. +Lula cresceu e prosperou enquanto tudo dava errado no pas. +Fcil subir nos palanques, defendendo que necessrio mudar tudo o que est a. +Seu sucesso estava, essencialmente, sustentado no fracasso. +No clima de sinistrose, as fragilidades do PT no ficaram to visveis opinio pblica: a dificuldade de se fazerem alianas capazes de sustentar Lula quando virasse presidente; os aloprados que conquistaram posies estratgicas no partido; a falta de generalizada convico sobre o valor da democracia. +No final da reunio, o premi italiano, Silvio Berlusconi, leu um documento de 45 pargrafos, pelo qual 140 pases consideram que o crime organizado o maior inimigo das democracias. +Segundo nmeros da ONU, os cartis do crime como a Mfia, a Yakuza japonesa e os cartis de Cali e Medelln, na Colmbia, faturam anualmente US$ 750 bilhes em atividades ilcitas. +Os contratos de gaveta, vistos pela CPI como mais um indcio de que as empreiteiras atuavam em cartel, so compromissos sigilosos entre as empresas. +O sigilo uma das exigncias contratuais. +Alm do contrato com a Servaz, h outros do mesmo tipo, envolvendo mais empreiteiras. +Os documentos foram encontrados em papel ou retirados de disquetes apreendidos na casa de Ailton Reis, diretor da Odebrecht. +Com a Servaz, segundo o documento obtido pela Folha a Odebrecht acerta o pagamento de US$ 110 mil mais uma porcentagem referente a obras de abastecimento de gua em Roraima, ainda a serem licitadas. +Alm disso, tem que se alterar o aparato jurdico para favorecer aes de negociao. +Todo esforo deve ser no sentido de transformar o local do trabalho em primeira instncia do processo trabalhista, com o poder de compor conflitos. +S assim se evitar a estratificao da legislao, permitindo que o sistema acompanhe de maneira dinmica as transformaes na economia. +A Justia do Trabalho passaria a tratar apenas dos conflitos individuais, que no sejam resolvidos no prprio ambiente de trabalho. +H a necessidade de uma representao permanente do trabalhador que converse com o empregador e signifique garantia de que no haver perseguio aos negociadores, diz Siqueira. +Foi enterrado ontem de manh no cemitrio Chora Menino, em Santana (zona norte de So Paulo), o ex-chefe do Departamento de Fotografia da Folha Waldemar Cordeiro, 63. +Cordeiro morreu devido a complicaes decorrentes de um enfisema pulmonar (diminuio da rea disponvel para troca de ar nos pulmes). +A patinadora Tonya Harding, banida das competies por ter planejado atentado contra a rival Nancy Kerrigan, ser o destaque de setembro da revista Penthouse. +Segundo o jornal USA Today, a revista publicar fotos nas quais Harding aparece mantendo relaes sexuais com seu ex-marido, Jeff Gillooly. +A legislao prev multa de 300% sobre o valor da venda para quem se recusar a emitir a nota. +Depois, haver campanha de fiscalizao e aplicao de multas para quem for pego em flagrante. +O objetivo da campanha aumentar a arrecadao dos impostos. + gratificante exibir opinies progressistas, dessas que fazem chover cartas de apoio no Painel do Leitor. +A verdade, porm, no d cartas nem votos. +A verdade ofende somos uns boais. +O fato que o caos sangrento de nossas ruas o resultado conjunto de nossas prprias aes, embora ningum individualmente tenha a inteno de produz-lo ou sequer tenha poder para isso. +A responsabilidade a um s tempo de todos e de ningum. +Ns, motoristas, pais e jovens brasileiros, somos os piores inimigos de ns mesmos. + um equvoco reduzir a liberalizao comercial ao neoliberalismo. +Afinal, a abertura comeou na economia brasileira movida por consideraes pragmticas e objetivas. +Simplesmente tornara-se impossvel, por razes tecnolgicas, comerciais e financeiras, estimular o desenvolvimento com base na substituio de importaes. +Tal poltica convertera-se, com o avano da globalizao econmica, em sinnimo de proteo ao atraso, ao desperdcio e ineficincia. +O governo agora apenas antecipou uma rodada de reduo de tarifas prevista para janeiro de 95. +E, com essa medida, pode matar dois coelhos. +De um lado refora a estabilizao, pois qualquer eventual aumento irresponsvel de preos depara-se agora mais rapidamente com a concorrncia de similares importados; de outro, ajuda a aliviar a supervalorizao do real, pois quanto mais importaes houver, maior ser a procura por dlares. +Fazer avanar uma reforma estrutural (como a abertura da economia) e, ao mesmo tempo, calibrar um processo de estabilizao no porm uma tarefa fcil. +Resta portanto saber se as medidas anunciadas, em tese adequadas, tero na prtica o efeito que se espera. +Com o dlar comercial j a R$ 0,85 e o governo dizendo que no h limite para baixo, a necessria abertura pode involuntariamente desaguar num arriscado escancaramento. +O livro de Marilena Ansaldi tambm oferece detalhes sobre uma das mais bem-sucedidas experincias culturais de So Paulo: o Teatro da Dana, que funcionou a partir de 1975 na Sala Gil Vicente do Teatro Ruth Escobar. +Esse palco, alm de revelar inmeros talentos que depois se refugiaram na Europa, como Sonia Motta, serviu para Marilena inaugurar a fase mais frtil de sua carreira. +BRIZOLA O 3 NO RIO, com 13 % . +Estava vivo o sentimento tnico e nativista. +Seu instrumento de ser ouvido era a fora da violncia. +Em dois pases esses ndios ainda no esqueceram o trauma da conquista: Cortz, no prprio Mxico, e Pizarro, no Peru. + o delrio que alimenta o Sendero Luminoso. + a vigorosa histria mexicana que surpreende em Chiapas. +O acordo no se tornou pblico, mas, agora, se sabe que foram feitas muitas concesses polticas. +Nas grandes cidades, multides imensas saram s ruas em apoio aos rebeldes. +A rebeldia no a revoluo, como bem acentua Octavio Paz. +A rebeldia um ato pessoal; a revoluo uma manifestao coletiva. +Mesmo asim, Van Himst pretende surpreender a defesa saudita, colocando dois atacantes velozes. +Alm do zagueiro Grun e do centroavante Weber, o lateral-esquerdo Vital Borkelmans encontra-se tambm com um carto amarelo e pode ficar de fora hoje. +Eles se intitulavam filiais da MIT Dealing Management Inc., dos Estados Unidos. +A empresa norte-americana informou PF que no tem filiais ou representantes no Brasil. +A MIT brasileira no aplicava o dinheiro. +O golpe foi denunciado por funcionrios da empresa. +A ao da PF comeou s 16h30 de anteontem e s terminou ontem s 15h30. +A MIT operava no mercado desde junho passado. +A PF ainda no levantou os nmeros exatos do golpe, mas acredita que pelo menos cem pessoas tenham perdido seu dinheiro. +Em nvel internacional, h um processo de conglomerizao de empresas que, ao permitir a integrao de softwares, conferiu aos grandes grupos vantagens expressivas sobre os independentes. +Tem que se pensar, para o Brasil, em uma empresa com modelo acionrio flexvel, que permita incorporar, em uma nica marca, os esforos individuais desses criadores e dispor da sinergia necessria para investir no mercado internacional. +O jogador disse que antes de voltar Espanha visitar o Estado onde nasceu, a Bahia. +Ontem levou, no Rio, os dois filhos a uma visita de rotina ao pediatra. +Se sentir que h um desejo coletivo, ele vai encaminhar as emendas constitucionais, disse Luiz Henrique aps uma hora de conversa com Itamar, ontem, no Planalto. +Luiz Henrique disse que o PMDB vai procurar os presidentes e lderes dos outros partidos em busca do consenso sobre a votao da reforma fiscal e tributria. +A aprovao das emendas considerada essencial para o Plano Real. +O corredor comear na marginal Tiet, na altura do Cebolo, passar pelo Ceagesp, praa Panamericana, Faria Lima, Lus Carlos Berrini e terminar, por enquanto, no shopping Morumbi. +O escritrio de Jlio Neves j comea a preparar novos estudos para o prolongamento deste corredor alm do shopping Morumbi, em direo ponte do Socorro. +No sei se fui claro: seria mais fcil um pinguim cruzar trs Saaras do que FHC comer carne de bode espremido a um magote de sertanejos suados. +Compreendeste? +No fosse a eleio, d. Luciano Mendes teria de negar Cristo trs vezes para que nosso senador empuleirasse no lombo de um cavalo, envergando aquele chapu de cangaceiro. +O caso de Lula semelhante. +O candidato do PT bateu na trave em 89. +Agora imagina que chegado o momento. +Sente o bafejar da sorte a acariciar-lhe a cara. +Os metalrgicos do Estado de So Paulo da base da Fora Sindical (data-base em novembro) romperam negociao com os setores de autopeas, forjaria e parafusos e preparam greve. +Os empresrios ofereceram 10,4 % em novembro para So Paulo, Osasco e Guarulhos e 6 % mais 6,27 % em maro para o interior. +A categoria pede 69,69%. +O Sindicato dos Metalrgicos do Rio e do Grande Rio (data-base em outubro) se rene hoje com representantes da Firjan (federao das indstrias do Rio) pela primeira vez desde o incio da greve da categoria, na tera-feira. +O sindicato pede 99% e os empresrios oferecem 13,56%. +A greve por empresa atingia ontem sete empresas e 12 mil empregados. +O governador da Flrida, Lawton Chiles, 63, resolveu processar o governo dos EUA para recuperar os US$ 739 milhes anuais que ele diz estar gastando em despesas com imigrantes ilegais em seu Estado. +Ele acusa o governo federal de no cumprir bem sua misso de impedir a entrada no pas de estrangeiros e de no repor o dinheiro gasto pelos Estados com eles. +A deciso tem evidentes objetivos eleitorais. +Chiles, que pertence ao Partido Democrata e ao mesmo grupo neoconservador que ajudou Bill Clinton a chegar Casa Branca, vai tentar se reeleger em novembro deste ano. +Sua popularidade est mais baixa do que nunca e um de seus possveis oponentes filho de George Bush. +O governo se comprometeu a no garfar a poupana, nas palavras do presidente do BC. +Estabeleceu que as tarifas pblicas vo todas marchar ao compasso da URV at meados de maio. +Depois ser a vez dos financiamentos rurais e, no incio de junho, acenar os rumos que podero tomar os pontos fundamentais do plano a poltica monetria e a poltica cambial. +Ao justificar sua necessidade, o presidente da Repblica abriu uma fresta para a realidade na propaganda sobre as virtudes da URV. +Mesmo indexado, o salrio convertido em cruzeiros reais continua a ser corrodo pela inflao. +Mais de 110 mil pessoas assistiram partida, no estdio Santiago Bernabeu. +O atacante Romrio no atuou bem. +Afastado de seu companheiro de ataque, Stoichkov, pegou pouco na bola. +Para Itamar, Stepanenko 'est perdido'. +O presidente Itamar Franco disse ontem que o ministro Alexis Stepanenko (Minas e Energia) autor de vrios bilhetes pedindo apoio ao candidato Fernando Henrique Cardoso est perdido. +Ele est perdido na China, disse Itamar, com bom humor. +Stepanenko viajou para a China h uma semana e deve voltar na sexta-feira. +A demisso do ministro dada como praticamente certa. +Ir a uma feira livre serve de aula para os principiantes aprenderem a comprar frutas, legumes e verduras mais baratos e de melhor qualidade. +Mas preciso disposio para andar a feira toda, com caderno e lpis na mo, comparando os preos dos produtos. +O advogado Mariano Gonalves Neto, autor da ao popular que se arrasta h 12 anos, acusa os ex-ministros de aprovarem uma superavaliao dos terrenos entregues pelo grupo Delfin como pagamento de uma dvida que, em 1982, chegava a Cr 70 milhes. +O ex-ministro da Fazenda Ernane Galvas (governo Figueiredo) disse que sua participao no episdio limitou-se a um de acordo no processo que teria vindo pronto do Banco Nacional de Habitao, subordinado na poca ao Ministrio do Interior. +O episdio emblemtico do tipo de poltica externa exercitada pelo governo Clinton e ajuda a explicar por que ela to mal avaliada. +At agora, o Bradesco Visa financiava em at quatro parcelas, com os juros cobrados sobre os cruzeiros reais. +Para o lojista, a nova modalidade no traz mudana alguma. +Ele receber o valor vista aps 30 dias. +O candidato Presidncia pelo PT (Partido dos Trabalhadores), Luis Incio Lula da Silva, o convidado de Marlia Gabriela no Cara Cara. +CARA CARA Bandeirantes, 0h. +Marco Antonio Nahum, advogado dos canadenses, nega que o premi venha ao Brasil tratar da expulso. +Isso ainda no ficou acertado pelo governo canadense, disse. +A famlia de Christiane j gastou aproximadamente US$ 1 milho para tentar a sua libertao. +Contratou a empresa Winfreys, a maior especialista em atividades de lobby, em todo o Canad. +Terminado o Mundial, todas as atenes das selees j se voltam para a Olimpada de Atlanta. +Um exemplo o tcnico norte-americano, Taras Liskevich, que no esconde seu objetivo: conquistar a medalha de ouro olmpica. +Para isso, a comisso tcnica no se cansa de estudar os outros times. +Nesta temporada, um dos mais visados foi o Brasil. +Ele filmaram todos os jogos das brasileiras. +A dcada de 90 no tem sido boa para a China. +O time, que nos anos 80 dominou o vlei feminino, voltou a decepcionar no Mundial. +Novamente, no conseguiu ficar entre os quatro melhores. +Em 92, na Olimpada de Barcelona, as chinesas terminaram em stimo lugar, colocao que provocou grandes mudanas na equipe. +O Santo Andr perdeu por 2 a O para o Rio Branco, de Americana, ontem tarde no estdio Bruno Jos Daniel, em Santo Andr. +Para o Rio Branco, bastou o primeiro tempo do jogo para determinar o resultado. +Souza, ponta-esquerda do time de Americana, abriu o marcador aos 25min. +Paulo Cesar, cobrando falta na cabea da rea, fechou o marcador aos 43min, sem chances de defesa para o goleiro Slvio, do Santo Andr. +Para se ter uma idia, no Brasil a melhor marca de Hilma, com 3,12 m.. +No por acaso, o tcnico Bernardinho diz que o time cubano ataca do terceiro andar. +Parar esse ataque no bloqueio uma misso quase impossvel. +O presidente Itamar Franco tem, como qualquer cidado brasileiro, o direito de apoiar a candidatura para a Presidncia da Repblica de quem bem entender. +Pode tambm cobrar dos membros de sua administrao que apiem o seu candidato ou deixem o governo. +Ainda assim, os planos do Planalto de dedicar apoio total ao ex-ministro Fernando Henrique Cardoso, como esta Folha revelou na edio de ontem, esbarram em problemas. +Embora no ocorra no Brasil h muito, absolutamente normal, nos pases de maior tradio democrtica, que o governo tenha um candidato e o apie. +Ronald Reagan no poupou esforos para eleger seu vice, George Bush, presidente dos Estados Unidos, em 1988. +A defesa apresentar o recurso extraordinrio no ltimo dia do prazo: trs dias teis aps a publicao no Dirio da Justia da deciso do TSE sobre o embargo declaratrio pedido pelos advogados de Lucena. +Os advogados de Lucena calculam que isso no deve ocorrer antes de tera ou quarta, com Lucena j eleito, segundo acreditam. +O embargo uma medida para que o TSE admita a constitucionalidade da questo o que permite que o assunto seja levado ao STF. +FHC -- No h temor de recesso. +No estamos fazendo poltica recessiva. +No estamos diminuindo investimentos do setor privado e, no setor pblico, o BNDES est mudando a orientao de investimentos do Nordeste. +Estamos num ciclo de expanso. +Folha -- Mas no momento em que a nova moeda for criada, se os preos forem convertidos no pico, no ser necessrio um aperto monetrio, uma recesso temporria? +A agncia de comunicao Free Press, do comit do candidato Fernando Henrique Cardoso (PSDB-PFL-PTB), reconheceu ter errado na divulgao de uma entrevista do diretor para Assuntos Internacionais do Banco Central, Gustavo Franco, concedida a ela como pea de campanha. +Na sua defesa encaminhada ontem ao TSE, ela admitiu ter cometido um erro de interpretao na produo da reportagem na qual Franco afirma que o PT, caso ganhe as eleies presidenciais, pode dar um calote na dvida interna. +Promotor pblico, procurador da Justia e um dos criadores da Comisso Justia e Paz da Arquidiocese de So Paulo, Bicudo lista como outras prioridades temas de sua especialidade: a reforma da Justia e do sistema penitencirio. +Folha -- Quais so suas linhas prioritrias de atuao na Cmara dos Deputados? +Para tentar se recuperar do jet lag e da correria da exposio do marido no Yerba Buena, Regina Cas fez ontem sada pela tangente. +Junto com seu guapo Luiz Zerbini, foi at Muir Woods um bosque nas imediaes da cidade, onde s queria era abraar rvores de preferncia, enormes sequias. +O grupo Po-de-acar, 2 maior rede de supermercados do pas, iniciou negociaes para a aquisio das 27 lojas G.Aronson, todas localizadas em So Paulo. +A Folha confirmou os entendimentos junto a empresrios do setor de eletrodomsticos e a funcionrios do Po-de-acar. +Oficialmente, a rede nega. +Folha -- Em torno dele no estavam pessoas que hoje esto na sua candidatura? +FHC -- Certamente, e da? +O candidato do PT apresentou o vice de seu principal adversrio como algum identificado com o autoritarismo. +At aqui, os tiros de Lula parecem no ter abalado o alvo. +Meu passado muito me orgulha, d de ombros Maciel. +O governador se convenceu ainda de que, se abandonassse a candidatura, poderia se tornar refm de Qurcia e perderia o poder de influncia na escolha do candidato do PMDB. +A declarao de Fleury de que continua candidato no convenceu alguns prefeitos que o acompanhavam. +Ele j est abraando a candidatura Qurcia, disse Itamar Borges (PMDB), de Santa F do Sul. +Fleury aproveitou a viagem para responder ao ex-secretrio Jos Machado de Campos Filho, que o chamara de traidor por no apoiar Qurcia. +Para quem quer ser candidato ao Senado, faltou bom senso e juzo, disse. +Ao visitar a ponte rodoferroviria de Rubinia, criticou o deputado quercista Alberto Goldman (PMDB-SP), ex-ministro dos Transportes. +Ele veio aqui e liberou US$ 5 milhes, 1% do valor da obra., declarou. +Colaborou Aurlio Alonso, da Folha Norte. +Apesar de todas as restries, a esmagadora maioria dos consultados (86%) acredita que Lula tomar posse sem resistncia de qualquer setor da sociedade. +H uma pequena parcela, no entanto, que catastrofista ao ponto de afirmar que Lula sequer conseguir tomar posse. +So apenas 2%. +Para outros 9%, haver resistncias mas no a ponto de inviabilizar a posse. +Burle Filho disse que combinou a ida de seu auxiliar aps telefonar para seu colega carioca, Antnio Carlos Biscaia. +Segundo Burle Filho, Biscaia no lhe adiantou nada por telefone sobre o assunto. +Queremos saber se o bicho de So Paulo e o do Rio so ramificaes de uma grande organizao criminosa. +Ao mesmo tempo, estamos esperando denncias sobre a atuao dos bicheiros paulistas, disse Burle Filho. +Mas o deixaram partir. +Infante -- E pagaram as passagens. +Me deram uma licena de dois anos, acreditavam que o exlio ia me neutralizar. +Tinha uma famlia para sustentar, e pensavam que isso me impediria de fazer qualquer coisa. +Pensavam que meu livro Trs Tristes Tigres seria recebido sem grande estardalhao. +Outro lado de Lara: ela estudou canto lrico, adora msica e costuma cantar em shows e jam sessions com amigos. +Como fazia em Madri. +A idia para o livro surgiu aps uma entrevista que Andra fez em meados de 93 para o Tokyo Journal com o jogador Zico, poca atuando no Kashima. +O editor da revista me telefonou e deu a idia. +Fizemos um projeto e comecei a elaborar a estrutura do livro, diz Andra. +Nos fundos de commodities h liquidez diria aps 30 dias. +Na transio para o real, so mais recomendados os fundos de renda fixa DI (Depsito Interfinanceiro). +RIO DE JANEIRO Aos trancos e barrancos, eis que mais uma vez o pas tenta dar um passo em direo a um futuro mais feliz. +O panorama assustador: ameaa de greve, possibilidade de hiperinflao e descontrole de preos, perda salarial, nuvens negras para todos os lados. +Exercendo o direito de no estar a favor ou contra ningum, gostaria aqui de remar contra a mar e pasmem manifestar meu otimismo. +Por que no? +Se h um artigo em falta no mercado hoje todo tipo de mercado, diga-se o otimismo. +Por que no ousar ainda que ingenuamente esperar que as coisas desta vez se ajeitem? +Olha o Carnaval de salo! +S bela! +S piranha! +S lulite! +Eu no s lulite, s gostosa. +Rarar! +E bomba! +Bomba! +Simo Emergncia! +Um camel cearense inventou o kit Aids: um par de joelheiras e 45 camisinhas! +Rarar! +Comeou a esculhambao?! + Carnaval! +E os nomes dos blocos? +Balana Rolha, Sacode a Rosquinha e Peru Esperto! +Bloco da Galinha Azul Solta a Franga. +Alpinista Sexual s Scala Monte Lbano. +No, eu vou pescar. +Ah bem, pensei que voce ia pescar. + mole? + mole mas sobe! +Principalmente no Carnaval! +T no ar mais uma calnia do Macaco Simo! +Pior o Itamar que ficou intoxicado com o prprio pastelo! +Nervosas! +Atacadas! +Inadimplentes do sexo em geral! +Tem pelado no salo! +Os Piranhos do Kung Fu invadem o salo! +Essa a grande novidade do Carnaval 94: os pelades. +Antes o Scala e o Monte Lbano s contratavam peladas. +Depois do Clube das Mulheres (em Portugal, Clube das Bigodudas) os pelades invadem os sales. +Gente, cada piranho! +Parece remador de porta avio! +E bem Clube das Mulheres: sunga, punho e gravatinha. +E eu sei que tudo bofe mas parece tudo gay. +Capa da revista Honcho. +Esttica gay! +Um rinoceronte de sunga mais sexy! +Pelo menos agora na televiso tem pra todo mundo. +No Brasil ns j estamos com oito tipos de opo sexual: perua, bofe, bofeca, boneca, traveca, hermafrodita, drag queen e drag king. +E tem gente que ainda fica na mo? +Rarar! +Carnaval devia ser o ano todo. +Cinco dias pra amador! +A fazenda de renas Napapiiri, nas proximidades (tel. 38-4048) promove jantares com carne de rena e faz exibies folclricas. +Preo: cerca de US$ 43. +Dos museus, o melhor o Arktikum (leia reportagem ao lado). +Mas h dois muito instrutivos, a cu aberto por isso, as visitas vo s de junho a setembro. +Achei a idia muito interessante e educativa. +A gente precisa de reciclagem em geografia, pois tudo muda muito rpido. +L em Portugal, eu costumo comprar a Folha, afirma Barra. +Em So Jos do Rio Preto, as duas bancas mais tradicionais da cidade tiveram filas na distribuio do terceiro fascculo do Atlas Folha/The New York Times. +Esses ncleos coloniais, entre eles o do Vale do Ribeira, foram ocupados a partir de terras vendidas pelo governo. +Para quem vai fazer a pesquisa no computador a separao de dados no faz nenhuma diferena: tudo funciona como se fizesse parte de um s arquivo. +Segundo Milton Julio Zanluqui, da Paraklin Pra-raios, entre os tipos mais comuns (veja exemplos ao lado) esto o sistema de gaiola (Gaiola de Faraday), composto de seis partes principais: captador do tipo terminal areo, cabo de cobre, suportes isoladores, tubo de proteo, malha de aterramento e conector de medio. +Esse equipamento envolve o prdio e indicado para construes que ocupem reas horizontais, como galpes e indstrias. + preciso que tenhamos a conscincia de que os nossos tratados, que envolvem questes transcendentais que buscam uma circulao de bens e servios sem barreiras entre nossos pases, exigem uma viso da histria. +No podemos deixar morrer o esprito de Iguau. +Ele fechou um tempo de divergncias para abrir uma era de convergncias. +Jos Sarney escreve s sextas-feiras nesta coluna. +O Rio sempre foi um lugar que deu exemplo para o Brasil, disse o corregedor eleitoral, tentando desfazer o carter de interveno branca atribudo sua viagem ao Rio de Janeiro. +Ns no vamos acreditar que um povo to culto como o da Guanabara possa ter problemas como andam dizendo por a, afirmou, referindo-se populao do antigo Estado da Guanabara, que desapareceu na fuso com o Estado do Rio de Janeiro. +Um, o convencional, o que o Plano Real contempla e tem como instrumentos essenciais a importao taxa cambial fixa e baixa e quase sem tarifa aduaneira de mercadorias, tornando impossvel pela competio os produtores e comerciantes brasileiros aumentarem seus preos; e a reduo da demanda por bens de consumo e de investimento, mediante juros altos e corte do gasto pblico, impedindo as remarcaes de preos pelo excesso de oferta suscitado. +O outro, heterodoxo e inconvencional, consiste em controlar as presses inflacionrias em sua origem, nas cadeias produtivas, mediante a fixao negociada entre todos os setores participantes de tetos para os aumentos de valores, sejam estes preos ou salrios. +A agitao ontem foi grande tambm na Bolsa de Mercadorias com frequentes boatos de dificuldades de instituies financeiras em cumprirem seus compromissos no mercado futuro do ndice Bovespa. +A projeo de juros acumulados para este ms no mercado futuro caiu de 3,76% no dia anterior para 3,71% ontem. +O impacto provocado pela jazzstica trilha de A Marca da Maldade, com temas influenciados por Stan Kenton e at por pianistas de bordel, chamou a ateno de Edwards, que em 1958 procurava um msico novo, talentoso e barato para compor o prefixo da telessrie Peter Gunn. +O jovem (34 anos), talentoso e barato Mancini entrou rachando. +A fanfarra que a seguir comps para Dizem que Amor (High Time) s no ficou tambm gravada na memria de todo mundo porque, daquela vez, Edwards nem sequer chutou na trave. +De todo modo, uma slida e longa parceria se configurava, alternando comdias e dramas cuja qualidade cinematogrfica variava infinitamente mais que as virtudes de suas trilhas sonoras. +Quando os dois acertavam juntos (Bonequinha de Luxo, Vcio Maldito, A Pantera Cor-de-Rosa), era uma festa para todos os sentidos. +E faturamento em dobro para o compositor, que no perdia uma chance de estourar no hit parade com uma cano. +Por duas delas, Moonriver e Days of Wine and Roses, ambas com letra de Johnny Mercer, ganhou o Oscar. +O baiano Dias Gomes, 70, est se preparando para ressuscitar um clssico da teledramaturgia brasileira. +Semana que vem, comea a reescrever Irmos Coragem, novela de Janete Clair que a Globo exibiu com enorme sucesso entre junho de 1970 e julho de 1971. +A emissora pretende lanar o remake em janeiro de 95, s 18h, no lugar de Tropicaliente. +No centro de todos esses nossos esforos de qualidade est o nosso cliente. + tudo pelo cliente. +A principal meta da melhoria da qualidade da Degussa no Brasil a satisfao total do cliente. + o cliente quem define os requisitos que teremos de cumprir e, at mesmo, superar, praticando qualidade em sua plenitude e obtendo, assim, seus benefcios, que so competitividade, lucratividade e satisfao. +Entendemos que palavras como respeito, ateno e tica, incluindo a eficincia na prestao dos servios, devem mais do que nunca, nortear nossa conduta nas relaes com nossos clientes. +PT, PDT, PC do B e os outros contras (partidos contrrios reviso) exigiram que as votaes do Congresso revisor comeassem pela questo tributria. +A nova reivindicao irritou os partidos favorveis reviso, que criticaram a proposta. +No dia 1 de maro, a fundao inaugurou mais uma escola. +Ela foi construda em Marlia (cidade onde foi fundada o banco) com capacidade para mil alunos. +Como nas outras escolas, ali os alunos recebem gratuitamente o ensino, o material didtico, o uniforme, a alimentao e tratamento mdico e odontolgico. +A excluso de Maradona, por ter jogado dopado contra a Nigria, abalou os jogadores. +O time, antes bastante aguerrido e solidrio, passou a discutir em campo. +A Argentina fez uma opo bastante ofensiva. +Variava entre um 4-3-3 e um 4-2-4. +O tcnico Alfio Basile tentou aproveitar seus melhores jogadores. +Ainda h muito o que fazer para desgravar completamente as exportaes de impostos indiretos, conforme permitem as regras do Gatt e impe a realidade de um ambiente internacional competitivo. +Mas a Unio foi ao limite em termos de desgravao das exportaes. +Antes da urgente reforma constitucional, a extenso da desonerao tributria das exportaes para todas as etapas da cadeia produtiva enfrenta dificuldades operacionais insuperveis no mbito federal. +Seria invivel estimar, por exemplo, com a acuidade necessria para a concesso de crdito fiscal, qual a incidncia efetiva de PIS e Cofins em cada etapa do processo produtivo anterior produo dos insumos usados diretamente na produo para exportaes. +Reginaldo Duarte pertence a uma famlia de polticos de Juazeiro do Norte (CE), mas nunca havia disputado eleio. +Sempre fugia da raia. +Achava que no tinha vocao poltica, explica. +Em 90 ele no pde fugir. +O Tasso Jereissati quis homenagear a minha regio e pediu um nome. +Fui escolhido e no queria aceitar. +Mas aceitei porque foi uma gentileza do governador, diz. +Jnice Tristo afirma que acompanha a vida poltica de Elcio lvares h duas dcadas. +Nunca havia disputado mandatos eletivos. +Acha que foi escolhido como suplente pela sua imagem empresarial formada em mais de 50 anos. +O tcnico Sacchi elogiou ontem a bravura de seus jogadores, especialmente de Baggio, com seu toque mgico para ganhar partidas. +Ainda assim, foi a mdia que amplificou as acusaes, ao divulg-las sem uma checagem mais aprofundada. +Culpa da mdia, ento? + primeira vista, sim, mas a que se chega ao verdadeiro n da questo. +Por partes: a lista de nomes mais completa foi ao ar durante o Jornal Nacional, da Rede Globo, por volta de 20h. +De que adiantaria os jornais do dia seguinte omitirem os nomes, se eles j haviam sido arrastados lama por um noticirio que atinge cerca de 60 milhes de pessoas, certamente mais do que a tiragem conjunta de todos os jornais brasileiros? +O treinador Levir Culpi no quis adiantar o substituto de der Aleixo. +Segundo ele, a sada do meia-ponta representa uma perda importante para o time. +Vamos com calma achar o substituto do der, disse. +Culpi, no entanto, acha que o time pode suportar a presso e confirmar a vantagem em So Paulo. +O time pode empatar a segunda partida para se classificar. +O assessor de comunicao da Reduc, Fernando Fortes, disse que os grevistas esto desrespeitando a determinao do TST. +O que nos preocupa o cumprimento da ordem judicial. +Os grevistas esto irredutveis. +O TST fala em manter a produo de combustveis e gs. +Se eles no substiturem o pessoal, vo estar praticando desobedincia judicial, disse Fortes. +Nenhum eleitor evanglico deve se sentir culpado por ter opinio diferente de seu pastor ou lder espiritual, diz a cartilha. +O texto defende o voto para a Presidncia baseado em programas de governo. +E condena a disseminao de boatos, como os que atingiram Luiz Incio Lula da Silva na eleio de 89. +A IRLF s aprova o aborto quando a gravidez coloca em risco a vida da me. +Mesmo quando a gravidez decorre de estupro ou quando o feto no tem chances de sobrevivncia, o aborto visto como crime. +A IRLF condena ainda a eutansia e os mtodos contraceptivos no naturais, inclusive a plula. +A tcnica tradiciona de tomografia no conseguia ver o abcesso no dente. +Com o uso do mtodo tridimencional, foi possvel localiz-lo e descobrir a causa da morte. +O artista, cujo nome de batismo Lus Carlos dos Santos, pagou uma fiana de CR$ 100 mil e foi liberado. +A Folha procurou falar com Melodia sobre o incidente. +O telefone da casa do msico permaneceu conectado a uma secretria eletrnica. +Foi barra pesada, pelo som pareciam armas superpotentes, no foi um tiroteio qualquer, afirmou Tatiana Marques Pedrosa, 24, estudante de psicologia. +Tatiana mora na rua So Clemente, Botafogo, em frente principal via de acesso ao morro. +Morando h seis anos ali, Tatiana disse que nunca viu um perodo to violento. +H dois meses que a coisa est pior. +Com tanta violncia, sinto uma angstia muito grande, afirmou. +A expectativa do Sebrae era que o volume de negcios chegasse a US$ 7 milhes na Fenit. +Em 95, no entanto, a participao dos pequenos subsidiados pelo Sebrae na Fenit vai diminuir. +Vamos reduzir o nmero de expositores para cem. +A idia privilegiar os de maior qualidade e as feiras estaduais, diz Souza. +Ao lado da Fenit foi realizada a Fenatec (Feira Internacional de Tecelagem), com representantes da Argentina, Alemanha, Frana e Estados Unidos. +O IPMF padece de males profundos. +Mas est correto o conselheiro Paulo Planet Buarque. +Analisado isoladamente, um imposto democrtico e que, se levado sua consequncia lgica, o Imposto nico, poderia consumar a revoluo tributria que o pas deseja e precisa. +MARCOS CINTRA CAVALCANTI DE ALBUQUERQUE, 48, doutor em economia pela Universidade de Harvard (EUA), vereador da cidade de So Paulo pelo PL e professor da Fundao Getlio Vargas (SP). +Foi secretrio do Planejamento e de Privatizao e Parceria do municpio de So Paulo (administrao Paulo Maluf). +Tambm nisto Bscoli estava em seu elemento: poucos podiam ser mais hilariantemente viperinos desde que a piada no fosse dirigida contra voc. +E havia outro motivo para que ele atrasse a ira de tantos: seu sucesso com as mulheres. +Cite uma cantora da bossa nova ou estrela da televiso dos anos 60 e h poucas chances de que ele no a tenha namorado sempre dando a impresso de que fazia isto por tarefa. +Pela bossa nova, namoro at o Trio Irakitan, dizia. +De acordo com Maluf, a equipe de elaborao do programa ter 30 dias para trabalhar e o nome mais cotado para coorden-la o do presidente nacional do PFL, Jorge Bornhausen. +Segundo ele, a parte econmica do programa poder ser elaborada pelo ex-ministro da Fazenda Gustavo Krause (PFL-PE). +Segundo Maluf, o programa de governo seria o ponto de convergncia para a discusso uma futura aliana eleitoral na sucesso presidencial. +Ser um programa capaz de unir todas foras que querem levar o Brasil para o Primeiro Mundo. +Um programa capaz de combater a inflao, o desemprego e que abra uma corrente de investimentos para o pas, disse Maluf. +Se eu fosse voc, passava a andar de txi especial e apresentava a conta concessionria. +Ligue imediatamente para o Procon (tel. 829-3055) e solte os cachorros. +O livro est em toda parte e eu sou daqueles que no conseguem ver a imagem em outra dimenso. +O que fao? +O Brasil um pas do Terceiro Mundo, ainda sem cara e sem personalidade, mas metade do seu territrio a Hilia, que tem fascinado os iluminados, de Humboldt a Euclides da Cunha; acontece que, no passo de Jeca Tatu em que caminha, o Brasil ameaa transformar a Hilia, no sculo 21, que j nos olha com cara feia por cima do muro, num gigantesco cinzeiro, cheio da cinza das rvores e da cinza dos ndios. +O Brasil s sair da sua mediocridade se se deixar incorporar Amaznia. +E jamais chegar a isto se no tiver a total dedicao de algum como Ricupero, um excepcional cadre, que em pouco tempo convenceu os brasileiros da sua inteligncia e competncia na vida pblica. +Quando Ricupero foi pinado da sua misso amaznica, lembrei Osvaldo Aranha, que dizia que o Brasil um deserto de homens e de idias. +A TVs Bandeirantes e TVA/ESPN abrem espao para o futebol americano hoje. +A Bandeirantes mostra o videoteipe de Kansas e Atlanta, s 17h45. +A TVA apresenta em VT a partida entre Detroit e Dallas, s 7h. +Promotores da Siclia pediram Justia local o julgamento de 23 pessoas, acusadas de conspirarem para assassinar Salvo Lima, poltico democrata-cristo morto pela Mfia pouco antes das eleies de 1992. +A polcia deu ordem para prender mais sete pessoas suspeitas de envolvimento. +O caso Lima coincide com a priso de 54 pessoas, como parte de uma operao anti-Mfia no oeste da ilha. +Ignacio Perea, 32, foi condenado a cinco penas de morte em Miami. +Ele sequestrou e violentou trs meninos com a inteno de lhes transmitir o vrus da Aids de que se sabia portador. +As penas foram para sequestro, violao e tentativa de homicdio. + a primeira vez nos EUA que um tribunal considera o vrus HIV como uma arma letal. +O modelo de privatizao previsto na MP foi organizado pelo presidente do BNDES, Prsio Arida. +No Planalto, no existe consenso sobre a MP. +Inicialmente, a equipe econmica chegou a estudar a privatizao das empresas do grupo Nuclebrs, alm de Angra 1 e 2, mas no chegou ao fim. +No apenas Marluce Dias da Silva a poderosa superintendente da Globo que considerada gnio em casa. +To tchans quanto ela, seu marido Eurico o dono de uma coleo de restaurantes estrelados no Rio. +O PDT prope uma auditoria para a dvida interna. +Os papis da dvida seriam obrigatoriamente trocados por outros de vencimento a longo prazo (a partir de 15 anos, segundo o documento). +O programa prope a monetizao da dvida interna (pagamento com dinheiro impresso) acompanhada da criao de uma nova moeda (assegurada a converso da poupana em termos favorveis) e imposio de rgido controle monetrio. +Essa proposta permitiria ao governo negociar em posio favorvel o resgate dos ttulos pblicos, j que o programa admite o no-pagamento parcial da dvida, assegurada a poupana. +O PDT pretende reduzir os impostos federais a quatro. +Sobre reforma agrria, o projeto prev terra para 20 milhes de pessoas com assentamento de comunidades em reas vizinhas a estradas vicinais. +O grupo suo baseia sua msica na combinao de barulhentas guitarras sampleadas, vocais guturais e pesadas batidas danantes. +Ao vivo, sua msica no difere muito do pop agressivo de Ministry, Young Gods a quem substituiu no BHRIF, Nine Inch Nails e KMFDM. +Os alemes, por outro lado, fazem um tecnopop assobivel e nostlgico no seu uso de sintetizadores analgicos anteriores ao surgimento da tecnologia dos samplers digitais. +Segundo o COE, eram remotas as chances de Lauro e Alex serem encontrados com vida porque eles haviam levado comida suficiente para apenas um dia. +S sobrevivemos porque encontramos duas cabanas de caadores com mantimentos e cobertores, disse Nascimento. +O PPR tentou derrubar a reteno das verbas de habitao e educao no primeiro turno, mas uma manobra do governo barrou a mudana. +Na ocasio, o PMDB apoiou o governo, mas agora no estaria totalmente unido para repetir a dose. +A equipe econmica considera que cedeu tudo o que podia durante a votao da emenda no primeiro turno. +O governo concordou em incluir no Oramento de 94 investimentos em educao nos mesmos nveis do ano passado e destinar para habitao trs vezes mais do que o registrado em 1992. +Os nmeros destes investimentos ainda esto sendo fechados pelo Ministrio do Planejamento. +O presidente Itamar Franco disse que sero mais de 50 demisses nas estatais do Ministrio dos Transportes por causa da desobedincia MP de converso URV. +Vrias empresas ligadas a este Ministrio converteram os salrios pelo pico e no pela mdia dos ltimos quatro meses. +Itamar disse que at agora poucas demisses foram efetivadas porque h sempre um processo legal e burocrtico que precisar ser cumprido. +Mas todos sero demitidos, acrescentou. +O presidente disse que pediu ao ministro das Minas e Energia, Alexis Stepanenko, que cheque tambm as estatais ligadas a ele para saber se tambm houve desobedincia. +Participa de reunio partidria em Maravilha (Santa Catarina). + tarde, vai a Belo Horizonte e visita o jornal O Estado de Minas em companhia de Hlio Costa, candidato do PP ao governo do Estado. + noite vem a So Paulo para participar do programa Fogo Cruzado. +Segundo Marcondes, o valor do patrimnio do hospital de US$ 50 milhes (R$ 42,7 milhes) e o total de dvidas, de R$ 50 milhes. +Aps a negociao das dvidas, a reabertura do hospital est condicionada ao destombamento peloo Condephaat (rgo que cuida do patrimnio histrico da cidade) de uma rea de 10 mil m2, que dever ser comercializada. +As frias nos EUA podem terminar no balco do consulado, onde o passaporte devolvido s 16h. +O da advogada Zilda (no quis dar o sobrenome), 49, veio junto com uma carta dizendo que seu visto foi recusado. +A Diviso de Vigilncia Sanitria informou que foram registrados 402 casos de clera, com quatro mortes, este ano no Estado. +Os municpios com maiores incidncias so Nossa Senhora do Socorro, com 85 casos, Aracaju, 66, e Laranjeiras, 46. +O Banco de Leite Humano da Secretaria Estadual de Sade montou um estande na praa Fausto Cardoso, centro de Aracaju. +O objetivo informar sobre a importncia da amamentao. +A atriz Cssia Kiss e a primeira-dama do Estado, Maria do Carmo Nascimento Alves, visitaram ontem o local. +Iniciado em 1958, o estudo de O Capital por jovens intelectuais da USP s terminaria cinco anos mais tarde. +Entre eles estavam figuras hoje conhecidas, como o filsofo Jos Arthur Giannotti, o crtico literrio Roberto Schwarz e o economista Paul Singer. +FHC foi o primeiro a tirar consequncias de tal estudo. +Em 1962, publica Capitalismo e Escravido no Brasil Meridional, primeira obra de sua autoria a se tornar referncia nas cincias sociais do pas. +Para melhorar a ventilao, podem ser criadas janelas nos telhados ou pequenos vos.com telas para evitar a entrada de insetos. +Os vos permitem a sada do ar quente, que fica na parte superior do ambiente. +Vidros e janelas do transparncia casa e facilitam a entrada do ar. +Mesmo com os ventos do litoral, suficiente o vidro de seis milmetros, no temperado. + uma soluo econmica e segura, diz Vinograd. +No piso, carpetes esto vetados. +Deve-se usar pedras (pedra gois e ardsias), madeiras no-brilhantes (que riscam facilmente) e cermicas no-esmaltadas. +Outra soluo, utilizada pelos decoradores Oscar Mikail e Fernando Rodrigues Alves na casa de praia do apresentador de TV Gugu Liberato, o piso com p de mrmore e cimento queimado branco. +Uma casa de praia tem que ser prtica, ao mesmo tempo que confortvel, diz Mikail. +Rubens Barrichello promete um anncio nos prximos dias. +Fica com o pacote Jordan-Peugeot. +Por enquanto, se dedica preparao fsica. +Est com quilos de sobra. +Christian Fittipaldi, por sua vez, negocia com a Tyrrell. +Seu Sauber pode acabar trocando o certo pelo incerto ao esperar por Karl Wendlinger. +A Diviso de Vigilncia Sanitria de Sergipe registrou nos primeiros 115 dias deste ano 87 casos de clera com uma morte. +A chefe da Vigilncia, Nolia Soares, disse que, apesar da existncia de um surto da doena, o controle da qualidade das guas dos rios, poos e lagos tem evitado a propagao do vibrio. +Aristides est esperando a publicao do texto do FSE aprovado pelo Congresso para concluir se a desvinculao dessas verbas atinge ou no direitos individuais. +Pela desvinculao, o governo fica desobrigado de cumprir os artigos da Constituio que mandam aplicar percentuais fixos do Oramento em educao e habitao. +Aristides tem defendido junto ao governo a aplicao do art. 212 da Constituio, que prev que Unio deve destinar pelo menos 18 % da receita de impostos educao. +Os Estados e municpios tm que destinar 25 % desta receita. +O papa Joo Paulo 2 escolheu a irm Emlia Ehrlich para ocupar uma das trs secretarias no snodo que acontecer em outubro. +A reunio de bispos discutir a vida de padres e freiras. + a primeira vez que o papa indica uma mulher para um cargo desse nvel na Igreja Catlica. +As bibliotecas do Vaticano podero ser consultadas em breve atravs da Internet, rede de computadores com milhes de usurios. +O material ser transferido para a memria de computadores. +O projeto das empresas Xerox e Ernst & Rubican. +Foram analisadas apenas a primeira amostra dos produtos e ainda deve ser feita contraprova. +A Otker e a Somel vo pedir contraprova. +A Mocotex vai sofrer anlise para saber onde ocorre a contaminao. +Uma das poucas surpresas na eleio anticlimtica por que passou o pas foi a alta incidncia de votos brancos e nulos nas eleies legislativas e para os governos estaduais. +Se abordo este tema aqui no para arriscar-me na anlise poltica, coisa que outros faro melhor do que eu. +Meu intuito tentar entender que papel teve a imprensa nessa histria. +So os garotos do Nabuco que vo administrar o negcio. +Deciso do Supremo Tribunal Federal permite aos micropartidos disputar a Presidncia. +O STF derrubou dispositivos da lei eleitoral que restringiam a participao dos partidos de acordo com sua representao na Cmara. +15 de maio -- Sarney desiste de disputar a prvia do PMDB contra Requio e Qurcia. +Ela vencida por Qurcia, indicado candidato Presidncia. +A absteno de 52 % dos peemedebistas credenciados a votar. +A primeira etapa do M2.000 Summer Concerts amanh em Santos e domingo no Rio, inaugura a temporada 94 de caa aos shows. +Espalhados pelos 7.408km do litoral brasileiro, os shows deste incio de ano tem l suas atraes internacionais. +Nada comparvel aos shows de Madonna, Michael Jackson e Paul McCartney no final de 93. +Saem os megas e entram os micros, minis e mdis. +A dupla planeja agora temporada de dolce far niente. +Em meio a tantos scuds por causa dos imbroglios de seu vice Jos Paulo Bisol, Luiz Incio Lula da Silva teve anteontem um providencial refresco. +Preocupado com a facilidade de comunicao de seu adversrio, Francisco Rossi, Mrio Covas acatou as recomendaes de assessores do presidente eleito, Fernando Henrique Cardoso, e se submeteu s tcnicas de marketing. +Avesso a um tratamento mais tcnico de candidatos durante as campanhas eleitorais, Covas protagonizou a mudana mais significativa no horrio eleitoral gratuito. +A vida de alunos como Z no fcil. +Eles tm que comparecer no Juquery de segunda a sexta-feira, das 8h s 17h. +Em um dia da semana, Z faz planto noite. +Fica mais de 36 horas ininterruptas no pronto-socorro. +Uma vez por ms, d planto no fim-de-semana. +Segundo ele, os traficantes tambm teriam interesse em negociar uma reduo em suas penas. +Eles tm medo de ser mortos, porque a coisa est ficando violenta demais. +Entregar-se, com a garantia de reduo da pena, seria um bom negcio, disse Maciel. +A diria do aluguel de um Uno Mille sai por R$ 63, com quilometragem livre, na Avis. +Na locadora Clean Car a diria do mesmo carro sai por R$ 72 com limite de 200 quilmetros por dia. +A Localiza tem diria promocional de R$ 41 sendo que cada quilmetro rodado custa mais R$ 0,26. +Na promoo o cliente ganha uma diria grtis a cada trs. +Um advogado nova-iorquino transformou seu apartamento no Brooklyn num paraso tropical, com rvores, um sol artificial e pelo menos cinco jacars na foto, os policiais no momento da retirada de um deles. +Os animais foram entregues custdia do zoolgico do Bronx. +Um snodo da Igreja Catlica concluiu ontem que as freiras devem ter maior poder e responsabilidade. +Mas postos de comando da igreja permanecero exclusivamente masculinos, disse o cardeal Eduardo Martinez Somalo em Roma. +Pargrafo 3 -- Da aplicao do disposto neste artigo no poder resultar pagamento de benefcio inferior ao efetivamente pago, em cruzeiros reais, na competncia de fevereiro de 1994. +Pargrafo 4 -- As contribuies para a Seguridade Social, de que tratam os arts. 20, 21, 22 e 24 da Lei n 8.212, de 1991, sero convertidas em URV e convertidas em UFIR nos termos do art. 53 da Lei n 8.383, de 30 de dezembro de 1991, ou em cruzeiros reais na data do recolhimento, caso este ocorra antes do primeiro dia til do ms subsequente ao de competncia. +Pargrafo 5 -- Os valores das parcelas referentes a benefcios pagos com atraso pela Previdncia Social, por sua responsabilidade, sero atualizados monetariamente pelos ndices previstos no art. 41, Pargrafo 7, da Lei n 8.213, de 1991, com as alteraes da Lei n 8.542, de 1992, at o ms de fevereiro de 1994, e convertidas em URV, pelo valor em cruzeiros reais do equivalente em URV no dia 28 de fevereiro de 1994. +A polcia do Mato Grosso do Sul vai combater a pesca predatria durante a piracema (poca de reproduo dos peixes), que termina no dia 31 de janeiro. +At esta data, a pesca est proibida. +Os pescadores detidos em flagrante usando tarrafas (redes de pesca) sero acusados de crime contra fauna, que prev de um a trs anos de priso. +A polcia de Arealva (386 km a noroeste de So Paulo) prendeu trs rapazes acusados de estuprar a funcionria pblica A.P.S., 30, no ltimo domingo. +Renato Batista Pedroso, 21, Douglas Jos da Silva, 21, e A.L.F., 16, esto incomunicveis. +Segundo o delegado Roberto Ilhesca, Silva teria confessado do crime. +Outros trs suspeitos esto foragidos. +Ele confirmou as idias que eu j tinha, um candidato srio, responsvel. +Sobre minhas questes em particular, na rea de educao, ele me impressionou muito. +Tem muitos conhecimentos gerais e faz digresses um pouco longas. +Daria a mesma nota para ele e para o Rossi. +A Nigria conquistou ontem o ttulo da 19 Copa Africana de Naes, ao derrotar Zmbia por 2 a 1, em Tnis (Tunsia). +O zagueiro Elijah Litana abriu o placar para Zmbia aos 3min de jogo. +O atacante Emmanuel Amunike empatou dois minutos depois e marcou o gol da vitria aos 2min do segundo tempo. +Uma proposta plausvel a de se fixar um prazo determinado, trs ou quatro meses, para que a sociedade se adapte voluntariamente URV, estabelecendo regras privadas de converso. +Findo esse prazo, a converso para URV seria obrigatria, conforme regras previamente fixadas pelo governo. +No h hiptese do Plano ser lanado antes do Congresso votar, primeiro, a emenda constitucional que cria o Fundo Social de Emergncia e, em seguida, o Oramento federal para 1994 com deficit zero. +E com o Carnaval no meio. +Isso significa que, tudo indo bem, a criao da URV fica para maro. +A atriz e apresentadora norte-americana de TV Ricki Lake e outras 14 pessoas passaram a noite de anteontem na cadeia em Nova York, depois de haverem realizado um protesto nos escritrios do estilista Karl Lagerfeld em Manhattan. +As 15 pessoas so membros da Peta (People for Ethical Treatment of Animals), entidade norte-americana que luta pelos direitos dos animais. +Elas se algemaram entre si e nos mveis do escritrio em protesto contra a utilizao de peles animais em criaes do estilista. +O cantor Stevie Wonder anunciou anteontem que sua turn norte-americana a primeira desde 1989 reverter fundos para campanhas contra a fome. +O dinheiro arrecadado com a venda do single Take the Time Out ir para uma campnaha organizada pelo American Express. +A falha ocorreu no momento de conferir, reembalar e repassar os pacotes de moedas para a tesouraria do banco. +Calliari explicou que a posio das embalagens permitiu a queda de dois sacos, que depois foram parar na lata do lixo. +Segundo Calliari, o barulho na agncia causado pela reforma do prdio abafaram o rudo da queda. +O diretor-geral da PF, coronel Wilson Romo, disse que recebeu recomendaes expressas para investigar tudo a fundo. +A vitria, porm, no foi suficiente para acalmar o ambiente no clube santista. +O presidente Miguel Kodja Neto ignorou a diplomacia que normalmente marca o futebol para acusar o ex-jogador e empresrio Pel. +Ele foi um timo jogador, mas um pssimo dirigente, disse Kodja, que quer o afastamento de Pel, assessor de assuntos internacionais, e do vice Samir Abdul Hack. +Pelechian -- Sim, e gosto. +Folha -- O sr. acredita ter influenciado estes filmes? +Joo Paulo Bordon, vice-presidente da Swift-Armour, alega no ter sido comunicado sobre a realizao da assemblia da empresa para ratificar a concordata. +O anncio legal foi publicado recentemente. +O grupo Bordon enfrenta briga de famlia. +O balano do Eldorado S/A informa que as irms Verssimo, filhas do fundador, ocupam cargos na vice-presidncia e diretoria do grupo. +Sem acesso s contas do grupo, elas informam que no assinaram o balano. +Elas no apenas restringem a liberdade e o futuro dos que tentam deixar Cuba como tambm oferecem expectativa de reformas democrticas foradas aos que ficam na ilha. +O principal objetivo norte-americano evitar a repetio da fuga de mais de 125 mil cubanos em direo Flrida ocorrida em 1980 a partir do porto de Mariel. +Na ocasio, Fidel Castro permitiu a sada de milhares de pessoas insatisfeitas com seu regime e abriu um novo espao para se manter no poder. +Profissionais com longa experincia em cargos gerenciais e administrativos devem se preocupar, ao fazer o currculo, em mostrar a essncia da carreira, resumindo suas conquistas e realizaes. +Muitos, no entanto, preferem descrever em detalhes toda sua trajetria educacional e profissional o que acaba deixando o currculo extenso e de difcil leitura. +O filme bom, divertido. +As mulheres jornalistas que esto na tela tm inmeros problemas. +Alguns at se parecem com os meus. +Mas nenhuma delas tem medo de no encontrar o carro na sada do cinema ou de ser agredida pelo guardador, ou de parar no sinal de trnsito na volta para casa. +Amanh e por mais 15 dias tem mais filme. +O de meia-noite timo, mas em Botafogo e acaba tarde. +O das cinco tambm bom, mas no shopping mais concorrido da cidade e no vai ter ingresso. +Nem vaga para estacionar, embora seja seguro. + a Companhia de Dana de Deborah Colker que faz a abertura. +Com figurinos de Tufi Duek e Yam Reis e festa doppo com a turma do ValDemente. +Em discurso feito na semana passada em Johannesburgo, Arafat conclamou os muulmanos a uma jihad pela cidade de Jerusalm. +O discurso, transmitido posteriormente por uma rdio israelense, colocou o dilogo em perigo. +Nenhum filme brasileiro foi escolhido, quebrando assim a expectativa criada em torno de Alma Corsria de Carlos Reichenbach e Mil e Uma de Suzana de Moraes. +O Brasil est assim fora das selees de Cannes-94. +Tampouco nenhum filme latino-americano passou pelo crivo da Quinzena dos Realizadores. +Ainda assim, expressiva a presena nas outras sees do festival. +Na competio estar o mexicano La Reina de La Noche de Arturo Ripstein. +A paralela Um Certo Olhar selecionou Los Naufragos do chileno Miguel Littin e Sin Compassion do peruano Francisco Lombardi. +Adeus America de Jan Schutte, O Casamento de Muriel de P.J. Hogan, 71 Fragmentos da Cronologia de Um Acaso, de Michael Haneke, Fresh de Boaz Yakin, Venus de Neve de Sotiris Gortisas, Costas s Costas , Cara a Cara de Huang Jianxin, Rainha Bandida de Shekkar Kapur, Sem Pele de Alessandro d'Allatri, Wrony de Dorota Kedzierzawska, Trs Palmeiras de Joo Botelho, Katia Ismailova de Valeri Todorovski, Os Silncios do Palcio de Moufida Tiati e o filme coletivo Homem, Deus e Monstro. +O adversrio que os romenos mais temem no nenhuma seleo, mas o cansao. +O fuso horrio ser um duro obstculo. +A tabela da primeira fase prev uma viagem Los AngelesDetroitLos Angeles para a seleo romena, em um espao de oito dias, na primeira fase. +O prprio agredido informou polcia de que no se tratava dos agressores. +No gramado, antes do jogo, representantes das torcidas dos grande times de So Paulo (Corinthians, So Paulo, Palmeiras e Santos) fizeram uma manifestao pela paz. +Como exemplo no combate violncia, o prefeito carioca cita a criao da Guarda Municipal, um grupo de atividades especiais que tem atuado mais na liberao de reas tradicionalmente ocupadas por camels. +Foram reabilitados 65 pontos da cidades, usados como pontos de venda de drogas e que viraram quiosques de flores, conta Maia. +A princpio, a dvida era sobre a necessidade de se jogar com dois volantes meias que tambm auxiliam na defesa, Dunga e Mauro Silva. +Esta armao d equipe um carter excessivamente defensivo, o que desagrada queles que esperam da seleo brasileira um futebol alegre, sempre visando ao gol. +O funcionamento bastante simples. +O cliente passar seu carto de dbito ou de crdito Bradesco Visa no terminal da empresa conveniada e fixar as datas de pagamento de suas compras. +Na data acordada, os computadores do Bradesco debitaro (saque) a conta do cliente e creditaro (depcsito) a empresa vendedora. +O objetivo facilitar a vida dos clientes e reduzir os custos da instituio financeira. +O custo do processamento dos cheques de US$ 0,70 por folha. +O do pr-datado eletrnico, US$ 0,15. +Sem educao, reforma ficar 'capenga'. +O perodo da tarde do Seminrio Internacional foi dominado pelo conceito de Qualidade Total. +No no mbito da empresa, mas de um pas inteiro. +Tema: mercado, relaes de trabalho e educao. +As duas palavras no chegaram a ser ditas, mas resumem o consenso: investir em treinamento nas empresas bom, mas no basta. +No domingo, em Campos (280 km ao norte do Rio), Brizola concentrou as crticas em Cardoso e Lula. +Disse que FHC incompetente administrativamente e que Lula inexperiente e que no gosta de trabalhar. +Segundo os organizadores, havia 6.000 pessoas no comcio s 21h, quando Brizola comeou a falar. +Quando ele terminou, s 22h10, metade das pessoas havia abandonado o local. +A aposentadoria , em tese, o tempo de realizar planos antes abandonados pela necessidade de trabalhar, mas nem sempre isso que acontece. +Muitas vezes as mudanas na vida do recm-aposentado levam a uma sensao de perda de capacidade. +Essas mudanas geralmente coincidem com a crise da meia idade, que definida no pela faixa etria, mas como um certo momento em que as pessoas reavaliam as escolhas feitas na sua vida. +Que a organizao investiu muito tempo e dinheiro na sensiblizao para o programa e que, agora, com eles. +Que no faria nada, a no ser cobrar os resultados do investimento. +Zagalo assistiu ao jogo entre Flamengo e Corinthians, domingo, no Maracan. +Elogiou dois jogadores flamenguistas e um corintiano. +Ele afirmou que os atacantes Magno e Svio, do Fla, podem disputar o torneio Pr-Olmpico, em maro de 1996 na Argentina. +Primognito de Tsunezaemon Maeda, Takayuki tinha trs anos quando o pai, a me e seis irmos chegaram a Ituverava, procedente de Sagaken, no Japo, para trabalhar como colonos na fazenda Santa Tereza. +Tsunezaemon, que havia sido barbeiro em Lima, no Peru, onde morou por 14 anos, acumulara dinheiro com o qual comprou casas e prdios ao retornar ao Japo. +Segundo Arnaldo Leite, coordenador do Programa de Capacitao Gerencial do Sebrae (Servio Brasileiro de Apoio s Micro e Pequenas Empresas), o programa se distingue de similares de outros Estados por ter aval prvio do Tribunal de Contas. +De acordo com Leite, o Tc considerou desnecessrias as licitaes nas operaes do programa porque o Sebrae uma entidade civil sem fins lucrativos. +Na tarde de ontem, diante de dezenas de jornalistas, Pertence zerou os computadores da Justia Eleitoral. +A operao uma garantia de que todo o sistema estar vazio para receber os nmeros oficiais da apurao. +Ao todo, sero usados cerca de 5.800 computadores. +Os de maior porte esto nos TREs (Tribunais Regionais Eleitorais) e no TSE. +O Contru (Departamento de Controle e Uso de Imveis) desinterditou ontem dez salas de cinema. +Trs do shopping Ibirapuera, seis do Belas Artes e o Cinearte, no Conjunto Nacional. +Continuam interditados o Cine West PLaza 3, o Comodoro e o Iguatemi, que s deve voltar a funcionar em dezembro. +A produtividade das fbricas de nvel internacional foi o dobro da verificada nas demais, enquanto a qualidade foi cem vezes melhor. +Elas registraram uma produo por hora de trabalho 43% superior s demais, afirma o relatrio. +A frequncia na entrega de mercadorias para os clientes tambm revela grandes diferenas entre as empresas world class e as concorrentes. +O ex-auxiliar de produo em cervejarias Joilson da Silva, 23, casado com um filho, pedia mais informaes sobre o abrigo. +Eles do comida pra gente nesse lugar? +Podemos dormir? +O motivo seria a indefinio das regras de converso dos aluguis. +O advogado Waldir de Arruda Miranda Carneiro, especializado em locao diz que a converso espontnea pode ser interessante dependendo da negociao entre as partes. +Se o inquilino conseguir um bom desconto na hora da mudana do aluguel de cruzeiros reais para URV negcio para ele, diz. +Isso porque ningum sabe quais sero os critrios da converso automtica, explica. +Uma parada no Boathouse Cafe (East 72 com a Park Drive North) pode ser uma boa idia para quem est indo ao Metropolitan pelo Central Park. +A lanchonete simples, mas faz um saboroso cachorro-quente por US$ 3. +Quem gosta de comida italiana, deve experimentar o Christo's (Lexington, quase esquina com a rua 49). +O cardpio varia entre pratos de massa al dente (cerca de US$ 15) e peixes grelhados (US$ 20). +H algo de novo nestes quatro filmes que Cac Diegues fez para a TV Cultura, em cima de quatro canes de Gil, Caetano, Chico e Jorge Ben Jor. +No maestria de virtuose, no rigor formal racional, no mensagem revolucionria, no filiao s artes conceituais, no achado de marketing. +Queremos tambm negociar com as indstrias rentabilidade mnima de 15%, diz Arajo. +Outra sada que ser buscada o aumento da venda de carne in natura, que hoje de apenas 30 % do total produzido. +A meta chegar a 50 % , diz Ferreira Jr. +Outro dia mesmo, durante a Copa, um jornalista ingls que escreve sobre o futebol para os principais jornais do mundo, reclamava dos poucos autores de expresso que escreveram sobre o fut. +No caso do autor de O Amanuense Belmiro (recomendo a leitura de um dos inaugurais romances urbanos brasileiros) no havia nada especial a demandar a pergunta feita atravs do Alcino. +Os agentes de turismo do Japo esto dando tratos bola para novo tour em solo brasileiro. +Querem organizar pacotes para visitas ao tmulo do tricampeo Ayrton Senna eternamente favorito de 10 entre 10 japoneses. +O nmero a soma dos US$ 8,6 bilhes arrecadados mais US$ 3,3 bilhes de dvidas das empresas (transferidas para os novos controladores) mais US$ 3 bilhes que o governo deixou de investir. +Montoro disse que sobre esses US$ 15 bilhes o governo deixa de pagar US$ 1,5 bilho/ano de juros anuais, mais US$ 1,5 bilho/ano em amortizaes de dvidas e mais US$ 2 bilhes/ano em aportes de capitais s empresas. +A produo de petrleo na Argentina aumentou 30 % entre 1991 e 1993. +O salto na produo atribudo ao sucesso da privatizao da indstria petrolfera. +De 1991 a 1993, a produo de petrleo local pulou de 20 milhes para 34,60 milhes de metros cbicos. +ltima dica: no espere subir ao cesto do balo para iniciar suas fotos. +A preparao para o vo, por si s, j um espetculo parte. +CIRO COELHO editor-assistente de Fotografia. +A ex-prefeita Luiza Erundina participa hoje da divulgao de um balano comparativo entre sua administrao 89 a 92 e o primeiro ano da gesto Paulo Maluf frente da Prefeitura de So Paulo. +O balano aponta para uma queda nos investimentos nas reas de sade, educao e habitao e reduo nos salrios da prefeitura. +Uma pesquisa da Associao Nacional de Agncias de Viagens (Abav) revelou que o setor foi incrementado 29 % em mdia desde a implantao do Plano Real. +Para o presidente da Abav, Srgio Nogueira, a expectativa que o mercado de viagens e turismo recupere este ano os mesmos ndices favorveis de 1990, ano em que foi registrado o maior volume de vendas em passagens areas. +As agncias movimentaram cerca de US$ 2,5 bilhes em 90. +Na sonolenta Eindhoven, cidade do Brabante (regio do sul da Holanda), seriedade e pontualidade so apreciadas. +Gostamos de gente que cala a boca e faz seu trabalho, como Ronaldo. +Romrio era querido, mas em certos bares ele no entrava, diz um taxista. +O tricolor, aps tantas decepes na temporada, vai s finais da Conmebol, caa-nqueis to inexpressivo que o prprio So Paulo inscreveu apenas jogadores jovens, que se denominaram Lees, ao invs do tradicional Expressinho. +Apesar disso ou exatamente por causa disso, conseguiu a classificao diante do Corinthians, que ficou no meio do caminho: nem escalou sua fora mxima, nem optou por um time de juniores com alguns reservas de reforo. +Assim, o tricolor conseguiu impor seu melhor conjunto. +Mas ter sido apenas isso? +Vamos confrontar os titulares que tudo perderam com esses meninos que so a derradeira esperana de um ttulo neste ano: enquanto os titulares de Tel privilegiam o meio-campo, com trs, quatro, s vezes cinco volantes congestionando o setor, o que, na prtica, no confere nem poder ofensivo ao time, tampouco proteo infalvel defesa, os garotos jogam com Mona, o nico volante tpico. +O resto composto de meias-armadores e atacantes. +Ser mera coincidncia? +Em respeito histria, diga-se que o mal que a UDN fez ao pas no decorreu de seu moralismo, mas de sua amoralidade. +O mal residia, enfim, no fato de que a poltica real da UDN se traduzia no que conspirava entre quatro paredes, sem uma antena parablica que lhe pusesse termo e sem uma imprensa independente que revelasse, livre de paixes partidrias, sua vocao para renunciar ao Estado de Direito ao toque da primeira corneta. +A imprensa independente no quer cassar de Itamar Franco o direito de fazer seu sucessor. +Tambm no lhe pede que cometa o maior pecado de um governante, que no governar e no servir ao pblico. +Que o faa, no entanto, todos os dias do ano, no apenas quando o pas est boca da urna, e nos limites da lei. +Embora j estejam com as chaves do imvel novo, s devem ir para l em fevereiro. +Nossa vontade era mudar antes. +O casal quer trocar o piso do banheiro, revestir o piso das reas sociais com madeira e instalar armrios. +Metade dos Estados e municpios do pas federativa e economicamente invivel, segundo Aspsia Camargo. +A sociloga e cientista poltica falou sobre o tema na ltima conferncia do encontro da Associao Nacional de Ps-graduao em Cincias Sociais (Anpocs). +Na noite de ontem, havia cerca de 3.000 pessoas retidas no aeroporto de Ezeiza, o principal de Buenos Aires. +Os vos da tarde de ontem entre Buenos Aires e So Paulo ou Rio foram retidos em Ezeiza. +Sei disso. +Procure saber se voc se entrega, ou se voc foge de suas emoes. +Mas no faa perguntas como esta, porque o amor no grande nem pequeno. + apenas o amor. +No se pode medir um sentimento como se mede uma estrada. +Se voc tentar medi-lo, estar enxergando apenas seu reflexo, como a da lua em um lago; no estar percorrendo seu caminho. +Eu estou decepcionado, afirmou Coulthard aps o treino. +O escocs atribuiu o seu desempenho ruim ao fato de desconhecer o carro sob as novas regras. +Pela primeira vez neste sculo, vastos setores das classes mdias ocidentais no conseguiro efetivar a proeza social -- cimento da democracia e do capitalismo -- realizada pelas geraes precedentes: garantir a seus filhos um nvel de vida igual ou superior ao que seus pais haviam desfrutado. +Ensino universitrio, contas de poupana nos bancos, investimentos em aes e imveis no asseguram a transmisso do patrimnio familiar dos pais para os filhos. +Esse quadro de inseguranas e de incertezas que j dura h duas dcadas contribui para transformar o integrismo, o racismo, a intolerncia poltica num formidvel desafio ao sistema democrtico neste nosso fim de sculo. +LUIZ FELIPE DE ALENCASTRO, 48, historiador, pesquisador do Cebrap (Centro Brasileiro de Anlise e Planejamento) e professor do Instituto de Economia da Unicamp (Universidade Estadual de Campinas). +Emlia conta a Adelaide, tambm, sobre o livro que est escrevendo, que trata da famlias mais importantes de So Paulo. +Adelaide critica Emlia por esta sair pouco de casa e, em seguida, deixa a sala irritada pela pouca ateno qu lhe dispensada pela me. +Cada curso dura dois sbados, em um total de 14 horas, e custa US$ 250. +As prximas datas dos cursos so dias 23 e 30 de julho. +Santana era morador antigo de Lambari d'Oeste. +Os trs cortadores de cana eram de Alagoas e estavam na cidade havia 15 dias. +Lambari d'Oeste (5.000 habitantes) virou municpio h dois anos. +Tal cobertura, assistemtica e voltada para buscar escndalos, leva-os, tambm, a trabalhar sobre fatos consumados. +No estamos concluindo que este comportamento intencional, mas sim que h uma forte predisposio cultural (preconceito) em criticar o Legislativo antes mesmo de conhecer tudo que cerca algumas decises. +Os agentes polticos, sociais e econmicos tambm tm de se reciclar, pois sua ao sobre o Legislativo paulistano s ocorre em funo de interesses especficos. +Que a democracia contempornea fortemente corporativa o sabemos, mas esperamos que no continue a ser to cheia de particularismos! +RUI TAVARES MALUF, 35, mestre em cincias polticas pela Unicamp (Universidade Estadual de Campinas), analista da Superintendncia de Experincias Metropolitanas Internacionais da Emplasa (Empresa de Planejamento Metropolitano da Grande So Paulo). +Vinte marcas criaram t-shirts especialmente para o evento, sob o tema Atitude Forum, Zoomp, Zapping, Reinaldo Loureno, Walter Rodrigues, Der Haten, Viva Viva, Armadilha, Ellus, Idice, Sucumbe a Clera, Cia. do Linho, Special K, Lorenzo Merlino e Herchcovitch. +Do movimento underground esto presentes Anderson Rubbo, Cac di Guglielmo, Divas e Alessandro Tierni. +O direo geral de Paulo Borges. +No galpo sero mostradas fotos dos bastidores dos lanamentos de moda em registros de Claudia Guimares, da Folha Imagem. +O mercado acionrio foi bastante agitado ontem. +A Bovespa (Bolsa de Valores de So Paulo) movimentou R$ 450,1 milhes. + o maior volume desde 1 de novembro ltimo (R$ 763,6 milhes). +O mercado trabalhou com a expectativa de aprovao, pelo Conselho Monetrio Nacional, de medidas de socorro a bancos em dificuldades. +O ndice Bovespa operou em alta durante toda a manh e incio da tarde, mas fechou em baixa de 0,34%, considerando as medidas do CMN como restritivas ao funcionamento do sistema financeiro. +Seria uma repetio do que ocorreu em 1989, durante o Plano Vero, quando desapareceram 51,87 % de inflao do ndice oficial de correo monetria. +Muitas empresas entraram na Justia para obter o direito de contabilizar essa corroso inflacionria em seus balanos, e ganharam as aes em primeira instncia. +Os processos continuam tramitando. +O dlar futuro na BM&F foi cotado a R$ 0,977425 para os negcios com vencimento no prximo dia 29. +O ndice Bovespa futuro fechou a 39.600 pontos, com expectativa de valorizao de 7,10 % ao ms. +Aposto uma boneca Barbie como os pais de crianas entre cinco e doze anos ainda vo perder vrios fios de cabelo procurando por um tal de Mighty Max debaixo do sof, no cinzeiro do carro ou atrs da geladeira. +Mighty Max um boneco de plstico, que convive com monstros e morcegos em um universo miniaturizado. +O brinquedo a mais recente sensao da apetitosa loja Fao Schwarz, da Quinta avenida, em Nova York. +Juan Manuel Fangio, sobrinho do argentino pentacampeo de Frmula 1, est testando na Califrnia um novo prottipo que vai dar o que falar na Frmula Indy. +Trata-se do Eagle uma reedio com motor Toyota e chassis Lola do legendrio carro feito por Dan Gurney. +Assinaram o acordo Luxemburgo, Blgica, Holanda, Itlia, Frana, Alemanha, Espanha, Grcia e Portugal. +No so signatrios Inglaterra, Irlanda e Dinamarca. +Os pases visitados podero exigir a comprovao de meios de subsistncia para a permanncia do turista. +Essa permisso de entrada de brasileiros, sem o visto, partiu de uma negociao entre Portugal e a Unio Europia. +Na segunda-feira, a me de Christian, Suzy Fittipaldi, dissera que gostaria de pedir ao filho para que abandonasse as pistas. +Ontem, ela foi ao aeroporto e se disse contente por t-lo de volta ao colo. +s 11h30 de ontem, no escritrio da Fittipaldi Promoes, empresa da famlia, Christian afirmou que, enquanto estiver motivado e achar boas as condies de segurana, continuar na Frmula 1. +Folha O Banco Central est se espelhando na experincia de algum outro pas, em operao semelhante? +Tavares No usamos nenhum modelo porque temos uma caractersitica muito nossa, que o tamanho do Brasil. + um continente e um pas de Terceiro Mundo. +Vai ter real chegando de barco, caminho, avio, todos os meios de transporte. +J tinha feito dieta, ginstica localizada, exerccios com aparelhos e no conseguia eliminar essas gordurinhas. +Achei que a 'lipo' seria uma boa alternativa. +A cirurgia durou duas horas. +Silvia tomou anestesia peridural (aplicada na regio da coluna) e pde retornar para sua casa no mesmo dia. +No Tatuap, onde a falta de atendimento comum nos fins-de-semana, havia ontem clnicos e cirurgies trabalhando. +Ningum foi encontrado ontem nas secretarias municipais e estaduais da Sade para comentar o assunto. +Na semana passada, o mercado de gado leiteiro foi marcado por uma surpresa: o gado girolando atingiu preo superior ao holands puro-sangue nos leiles. +Dia 19 ltimo, o 2 Special Girolando atingiu a mdia de R$ 3.600 por 44 fmeas. +Dois dias depois, o Salute Milk, baixou a mdia para R$ 3.000 na venda de metade das 30 vacas holandesas que foram apresentadas para negcio. +Segundo o Instituto Mdico Legal de Taboo da Serra, todas as vtimas levaram mais de um tiro, a maioria na cabea. +A arma utilizada foi uma pistola automtica 380. +Os crimes no Itaim Paulista ocorreram na rua Enseada de Itapacoria, 327, por volta das 20h de sbado. +Muller chega s 5h25 de hoje a So Paulo, vindo de Londres. +O jogador contou o motivo que o obrigou a no acertar com o Everton. +No garantiram casa, carro, assistncia mdica e passe livre depois de quatro anos, disse o diretor Kalef Joo Francisco, que falou ontem com Muller. +O contrato, diz ele, expirou ano passado e s foi renovado em fevereiro. +Ele disse que a receita da ferrovia no cobre as despesas. +Gazeta Filho confirmou que os dormentes se deterioraram muito antes do previsto. +Segundo ele, os cerca de mil dormentes podres sero trocados em curto prazo. +O percurso pelas avenidas Consolao, Rebouas e Francisco Morato durou exatos 20 minutos, sem trnsito moroso. +Foi possvel cumprir os 13,4 quilmetros do percurso em uma velocidade mdia de 40 km/h, incluindo-se a espera nos semforos. +O que corresponde a uma rentabilidade de 13,03 % sobre o patrimnio lquido. +O grupo Votorantim recebeu o ISO-9002 para o cimento Votoran, produzido em Volta Redonda. +Os servios de inteligncia da Argentina que trabalham com a ajuda do Mossad (servio secreto de Israel) e do FBI (Polcia Federal dos EUA) atribuem o atentado ao Hizbollah, grupo muulmano xiita pr-Ir. +O grupo e o governo iraniano negaram envolvimento. +Ontem, o juiz Juan Jose Galeano, responsvel pelo caso, viajou para a Venezuela. +A corrida sucessria comea esta semana com um quadro mais claro e definido do que o da semana passada. +Embora ainda no possa ser definitivamente descartada, a hiptese da candidatura do ex-presidente Jos Sarney encontra-se pelo menos momentaneamente bastante afastada, abrindo espao para que a campanha comece a deslanchar com base no atual grid de largada. +A nica alternativa que resta a Sarney de fato que o Superior Tribunal de Justia acate a denncia de estelionato apresentada peloo Ministrio Pblico Federal contra Orestes Qurcia e que, neste caso, o ex-governador de So Paulo renuncie candidatura do PMDB. + uma possibilidade remota. +No s a expectativa de uma tramitao demorada at a deciso do STJ o que pode inviabilizar uma eventual candidatura Sarney por falta de tempo at as eleies, como tambm h informaes dando conta de que o ex-presidente poder em breve anunciar seu apoio a Fernando Henrique Cardoso, do PSDB. +Estudo da Associao dos Engenheiros da Petrobrs sobre a estrutura de preos dos combustveis mostra que a Petrobrs tem uma margem de at 29% no preo do combustvel que poderia ser usada para reduzir o valor da gasolina. +O clculo baseia-se na comparao dos preos brasileiros com o americano. +A Folha procurou a Petrobrs mas no obteve retorno. +Os Bulletin Board Systems (BBS) esto oferecendo um novo servio aos seus usurios: a conexo com a rede das redes, a InterNet. +Essa rede, criada h 20 anos pelo Departamento de Defesa dos Estados Unidos para conectar os seus vrios centros de pesquisa, considerada a maior rede de comunicao entre computadores, contando com milhes de usurios espalhados por vrias cidades do mundo. +Os servios mais procurados na InterNet so correio eletrnico e transferncia de arquivos, conhecida como FTP (File Transfer Protocol). +Para se ter uma idia da importncia da InterNet, a Casa Branca est conectada a essa rede, disponibilizando publicaes e documentos para qualquer pessoa do mundo. +O acantonamento Itere oferece futebol, vlei, caminhadas, brincadeiras, e est com vagas para as temporadas de 17 a 23 e de 24 a 30 de julho. +Fica na fazenda Itere, em Juquitiba, a 120 quilmetros de So Paulo. +Steven quer aproveitar a viagem ao Brasil para completar sua discografia dos Mutantes e conhecer Arnaldo Baptista. +A advogada diz que o Mais! adulterou a obra para satisfazer a vontade de neonazistas brasileiros. +Vera Lucia Vassouras, que negra e se diz militante do PDT, sustenta que os autores do livro em nenhum momento afirmaram que os negros so geneticamente inferiores ao brancos. +No texto publicado no Mais!, os autores do livro afirmam que as pontuaes de QI aumentam com o status econmico, em ambas as raas. +Mas a magnitude da diferena entre negros e brancos nos desvios padres no se reduz. +Na realidade, ela aumenta medida que as pessoas ascendem a escada socioeconmica. +H excees. +Uma blusa preta pode ser usada com um suti branco se a saia for branca, explica. +A angua deve ser usada sob roupa transparente. +Nunca use angua curta com uma saia comprida. +CONCERTOS INTERNACIONAIS GLOBO, 0h. +A comdia romntica Suburbano Corao conta a histria de Lovemar (Andra Beltro), uma sonhadora costureira do subrbio que idolatra o locutor de rdio Rogrio (Marco Nanini). +Adaptado da pea homnima de Naum Alves de Souza, o especial tem ainda no elenco Marisa Orth, Pedro Paulo Rangel e Diogo Vilela. +A direo de Guel Arraes. +Alm de Braslia, devem atrasar o relgio os Estados de So Paulo, Rio Grande do Sul, Santa Catarina, Paran, Rio de Janeiro, Minas Gerais, Esprito Santo, Gois, Mato Grosso do Sul, Mato Grosso, Amazonas e Bahia. +A alterao deve ser feita de acordo com a hora local, respeitando portanto as diferenas normais de fusos horrios. +Quem est duas horas mais cedo que Braslia mudar o horrio duas horas antes, portanto. +Com a mudana, o metr de So Paulo vai funcionar hoje uma hora a mais, como se tivesse duas meias-noites. +Os relgios sero atrasados meia-noite para 23h, e os trens continuaro a correr at a segunda meia-noite. +Isso vale para os trens das linhas Norte-Sul e Leste-Oeste. +O metr Paulista continua fechando s 20h30 e, portanto, no ser afetado pela mudana; as outras linhas do metr funciona das 5h00 s 24h00. +A proliferao nessa rea maior pelo fato dos indgenas no pagarem impostos com a atividade, mas apenas taxas. +Muitos dos cassinos tm grandes grupos por trs, que usam incentivos aos ndios como fachada. +A emoo cresce quando atravesso o salo de festas e chego s salas do museu, do Memorial de Menininha do Gantois. +Aqui era o quarto pobre, simples, limpo e acolhedor. +A cama no era um leito de enferma, era um trono de rainha. +Apoiada nos travesseiros, o busto levantado na animao da conversa, o rosto concentrado no jogo dos bzios, no instante da adivinhao, Menininha do Gantois personificava a verdade do Brasil, de um Brasil mais profundo e mais belo, situado alm da corrupo, da injustia, da violncia, da mentira, das pequenezes, da delao transformada pelos pobres homens da baixa poltica em suprema virtude nacional. +Ai, me Menininha, acode-nos nesta hora de quase desespero, d-nos o alimento da confiana e do sonho. +Aqui, neste espao onde se reverencia sua memria, eu a recordo amiga de toda uma vida, nossas longas vidas vividas na intensidade da paixo: com ela aprendi a bondade e o povo. +Me ensinou que s o povo constri grandeza e o faz desinteressadamente, no dia-a-dia da generosidade. +O Instituto Brasileiro do Patrimnio Cultural reinaugura dia 18 o Museu Victor Meirelles, em Florianpolis. +O museu funciona na casa em que nasceu Meirelles, autor do quadro A Primeira Missa no Brasil, sua obra mais famosa. +O prdio foi restaurado. +A Cartilha da Segurana Escolar foi lanada anteontem, em Porto Alegre. +O autor do projeto, vereador Isaac Ainhorn (PDT), disse que o objetivo orientar os estudantes sobre segurana pessoal, no segurana no trnsito e preveno contra as drogas. +O patrimnio era de CR$ 26,3 trilhes, equivalentes a US$ 16,3 bilhes, contra US$ 6,9 bilhes dos fundes tradicionais, que vm encolhendo. +A grande vantagem dos fundos de commodities a possibilidade de saques dirios, sem perda do rendimento, aps 30 dias. +Um grupo de miul policiais militares dos EUA sob o comando do ex-chefe de polcia de Nova York, Raymond Kelly, comeu a trabalhar ontem em Porto Prncipe com o objetivo de ajudar a manter a ordem pblica. +Trezentos soldados de cinco pases da regio do Caribe tambm chegaram ontem ao Haiti com a mesma misso. +O coordenador da Inteligncia Fiscal da Receita, Marcos Vincius Lima, disse que foram encontrados em 14 estabelecimentos de um conceituado colgio privado de So Paulo os equipamentos importados por uma fundao educacional ligada ao mesmo grupo. +Esse colgio foi multado semana passada em 2 milhes de Ufir quase R$ 1 milho, valor que corresponde ao II (Imposto de Importao) e IPI (Imposto sobre Produtos Industrializados) sonegados por meio da fraude. +Manoel Carlos Marques Beato, do restaurante Fasano e Jos Sebastio Figueiredo, sommelier e proprietrio do La Bicocca, garantiram suas vagas no Concurso Brasileiro de Sommeliers. +No segundo semestre, o concurso escolher um destes profissionais (responsveis pelo servio de vinhos num restaurante) para representar o Brasil no concurso mundial, no Japo, em 95. +Jack Girafa Charlton, tcnico da Irlanda, usou seu pescoo comprido e seus quase dois metros de altura para vislumbrar a passagem da equipe para as quartas, depois de um estria vitoriosa contra a Itlia na primeira fase. +Acabou saindo nas oitavas. +Com a desclassificao, Jack Girafa no teve o comportamento pacato que se espera da gigante das savanas. +Criticou a Fifa at pelas diretrizes sobre suprimento de gua durante as partidas. +Embora seja concorrente respeitado, a Nielsen no representa uma ameaa real, diz Flvio Ferrari, diretor da Ibope Mdia. +Acrescenta que o cinquentenrio instituto que, a exemplo da Nielsen, tem atuao internacional, se dispe a oferecer eventuais relatrios diferenciados do concorrente. +No final do ltimo ano, Coelho dividiu a cena com o tenor espanhol Plcido Domingo. +H um ms, estrelou Salom, de Richard Strauss. +O jornalista LUS ANTNIO GIRON viaja ustria a convite da agncia Austraca de Turismo e da Lufthansa. +No sbado, dia 2, a CET (Companhia de Engenharia de Trfego) restabeleceu o sentido normal de circulao da rua Visconde da Luz, no Itaim-Bibi (zona sul), da Joo Cachoeira para a Clodomiro Amazonas. +A mudana foi determinada como opo de retorno ou acesso avenida Santo Amaro. +Parte do crescimento de Fernando Henrique na ltima pesquisa Datafolha deve-se evoluo do tucano entre os eleitores que declaram simpatia pelo PMDB de Orestes Qurcia e pelo PPR de Esperidio Amin. +FHC foi de 33% para 41% entre os peemdebistas (oito pontos percentuais de crescimento) e de 3% para 42 % no eleitorado do PPR (11 pontos). +24 de novembro de 92 -- Um Boeing 737, da China Southern Airlines, bate em um morro pouco antes de aterrissar na cidade turstica de Guilin, causando a morte de 141 pessoas. +23 de julho de 93 -- Um jato britnico Aerospace 146, da China Northwest Airlines caiu na regio autnoma de Ningxia Hui. +Dos 133 passageiros e tripulantes que estavam na aeronave, 55 morreram. +Pergunta Quais suas opinies sobre a atuao do Ministrio Pblico na investigao da contraveno? +Resposta Injusta principalmente quando nos acusa de trfico de drogas, de participar de quadrilha e sequestro. +A Rscal Pizza & Cozinha criou um cardpio especial para o almoo da Pscoa de amanh, com uma massa com um recheio e um molho base de peixes. +Pelo preo de CR$ 6.200, os clientes podero se servir vontade do ravili com recheio de salmo. +Os que preferirem carne, podem optar por um nhoque preparado base de vitela. +A pizzaria foi inugurada pelo Grupo Viena h dois meses e oferece 21 tipos de pizzas no forno lenha, alm de massas especiais, grelhados e saladas. +Presidente arruma cargos para integrantes do 'Grupo de Juiz de Fora'. +Na reta final de seu mandato, o presidente Itamar Franco decidiu arrumar empregos, a partir de janeiro prximo, para seus amigos que compem o chamado Grupo de Juiz de Fora. +Parecia concurso de perguntas idiotas a cobertura do carnaval nos sales, tera. +Manchete, Gazeta e Bandeirantes empataram. +Voc se acha louca? +Os cargos oferecidos so, por exemplo, gerncia de produo industrial, engenharia de projeto ou desenvolvimento de produtos. +Pesquisa da Laerte Cordeiro & Associados, com base em anncios de emprego, indica que a rea voltou a ocupar a segunda posio na demanda. +Pases ex-comunistas como Polnia e Hungria tm interesse em entrar na Otan. +Eles temem, porm, que um relacionamento especial entre Moscou e a aliana os deixe sem garantias de segurana. +De seu lado, a Rssia manifestava preocupaes semelhantes diante da aproximao da Otan em relao aos antigos membros do Pacto de Varsvia (a aliana militar pr-URSS, extinta em 1990). +So trs as certezas entre as partes que negociam o aumento de salrio para o funcionalismo: a deciso no pode ser adiada, a aparncia de equilbrio fiscal deve ser preservada e Ricupero ficar no cargo em quaisquer circunstncias. +Comandantes militares voltaram a receber comunicado esta semana, destinado tropa, dizendo que o reajuste do soldo sair este ms. +Os maestros candidatos ao prmio de melhor regente so Claudio Abbado, Simon Rattle, Valery Gergiev, Mariss Jansons e John Eliot Gardiner. +Para melhor cantor, figuram Jos Carreras, Thomas Hampson, Ben Heppner, Sergei Leiferkus e Bryn Terfei. +Entre as mulheres, esto Cecilia Bartoli, Jessye Norman, Anne Sofie von Otter, Dawn Upshaw e Galina Gorchakova. +O Classical Music Award promovido pela BBC Music Magazine e pelo jornal The Independent, e tem o patrocnio da empresa Kenwood. +Outros prmios incluem personalidade do ano, melhor produo de pera, melhor transmisso de TV, melhor grupo de msica antiga, entre outros. +Antonio Jorge, diretor tcnico da Amil, diz que a empresa ainda estuda a converso, mas que os carns de junho j sero emitidos em URV. +Ele garante que o aumento real nas mensalidades no vai superar os 22 % propostos pela Abramge. +Outras empresas, entretanto, vo praticar um reajuste maior na converso. +Pssimo comeo de temporada para os dois melhores tenistas do mundo. +Pete Sampras e Michael Stich perderam no Torneio de Doha, no Qatar, para jogadores mais acostumados ao calor forte o marroquino Karim Alami e o haitiano Ronald Agenor. +O nico favorito a vencer foi Stefan Edberg, que arrasou o tambm sueco Anders Jarryd por 6/1 e 6/1. +Sampras esteve irreconhecvel. +Alami, de 20 anos e nmero 204 do ranking, teve a maior vitria de sua carreira. +Tinha vo marcado para esta noite (ontem), porque no imaginava ser capaz de vencer Sampras, disse Alami. +SO PAULO -- Se o leitor se deu ao trabalho de ler todas as repercusses sobre o real publicadas ontem pela Folha, ter verificado que, como sempre ocorre nessas ocasies, h opinies contrrias, a favor e mais ou menos. +Se separar as opinies conforme a caracterstica do entrevistado, ter verificado igualmente que todos os empresrios ouvidos so a favor. +Seria bom grifar a o todos. +Os 160 expositores da mostra da indstria de cosmticos estimam vendas de US$ 150 milhes at a prxima segunda-feira. +So esperados cerca de 60 mil visitantes. +A Cosmtica'94 acontece no Parque Anhembi (zona norte de SP) e aberta ao pblico. +A Secretaria das Finanas da Prefeitura de So Paulo informou ontem que a partir de segunda-feira, dia 8, a UFM (Unidade Fiscal do Municpio) valer R$ 28,15. +O reajuste de 6,08 %, equivalente ao IPC-r de julho. +Os contribuintes que pagarem seus tributos (IPTU, ISS etc.) hoje sairo ganhando, pois o clculo ser feito pela UFM de R$ 26,54. +Ser lanado no prximo dia 9, no auditrio da Folha, a partir das 19h30, o livro Tributao no Brasil e o Imposto nico, organizado pelo economista Marcos Cintra. +Autor da proposta do Imposto nico, que pretende reduzir a enorme gama de tributos a um, Marcos Cintra far uma palestra sobre o assunto. +Ainda em setembro, no final do ms, encerram-se as inscries nos colgios Santa Cruz (para a 1 srie do 1 grau) e Santo Amrico. +As escolas limitam os vestibulinhos a algumas sries que tm maior procura por vagas. +MAIORIA DOS PROGRAMAS EST LIGADA REDE DE ESGOTOS*. +A modelo Monique Evans, um dos destaques do Carnaval carioca deste ano, vai submeter-se a uma tomografia computadorizada na prxima semana para verificar a origem de um pontinho que apareceu em seu rim. +No acho que seja cncer, pode ser algo relacionado aos meus medos e angstias, afirmou. +Monique, 37, disse que descobriu a marquinha, que no pedra no rim quando se separou do marido, em junho passado. +Senti dores e, como tenho uns casos de cncer na famlia, decidi fazer o exame, contou a modelo. +Antonio Delfim Netto, deputado federal pelo PPR-SP, ontem no Jornal do Brasil. +Itamar e as autoridades que o acompanhavam chegaram ao cemitrio 1h50 de ontem. +Ele permaneceu no velrio por aproximadamente uma hora e seguiu para descansar no hotel Glria at o horrio do enterro. +Segundo amigos do presidente, ele repetiu vrias vezes que Ariosto era o filho homem que nunca teve. +Ele voltou a ter crises de choro. +Na dianteira, o freio a disco ventilado de 320 mm, com pisto duplo. +Na traseira tambm h disco ventilado de 230 mm. +Ambos tm acionamento hidrulico. +A suspenso frontal por garfo telescpico invertido, marca Marzochi, com curso de 200 mm. +A suspenso traseira progressiva com um s amortecedor Boge. +O TJM (Tribunal de Justia Militar) vai comear a ouvir neste ms as testemunhas de acusao do processo contra os 120 Policiais Militares denunciados pelo massacre de 111 presos no pavilho 9 da Casa de Deteno, no Carandiru (zona norte de So Paulo). +O massacre ocorreu quando a tropa de choque da PM invadiu o pavilho para acabar com uma rebelio, em 2 de outubro de 92. +Mal, pelo menos por enquanto, na eleio presidencial, o PMDB mostra na corrida dos senadores que ainda tem cacife. +So peemedebistas os lderes no Rio Grande do Sul (Jos Fogaa), Paran (Roberto Requio) e Cear (Mauro Benevides). +No espectro poltico, Fogaa e Requio so rotulados como membros da centro-esquerda. +O Conselho Municipal de Defesa dos Direitos das Mulheres e das Minorias promove hoje em Natal, a partir das 14h, no calado da rua Joo Pessoa (centro), um ato pblico. +A Coordenadoria Especial da Mulher de Londrina (PR) promove debate, s 20h30, no Catua Shopping Center, com a sexloga Marta Suplicy. +Em Curitiba, as comemoraes vo incluir envio de fax, plantio de rvores e passeatas. +Em Salvador, haver hoje a inaugurao na estao da Lapa (centro) de um balco de informaes sobre as questes da mulher. +Em planto especial, funcionrios da Delegacia Regional do Trabalho e do Instituro Pedro Melo expediro carteiras de trabalho e de identidade para mulheres. +A excurso est sendo organizada pelo clube More Fun, que promove a troca de correspondncias entre gays que no querem expor publicamente suas preferncias sexuais. +At sexta-feira, 38 pessoas j haviam reservado suas passagens. +Queremos lotar dois nibus com a 80 pessoas e fechar o hotel, disse o administrador de empresas E., 31, um dos organizadores. +Curiosamente, quanto mais os novos programas e redes facilitam a vida dos informatizados, maior a tendncia dles de ficarem grudados no computador. +Hoje em dia, qualquer americano medianamente equipado pode fazer diante do teclado tarefas que antes exigiam que ele tirasse o bumbum da cadeira: mandar e receber fax e mensagens eletrnicas, reservar passagens areas, controlar a conta bancria, acessar bancos de dados e dezenas de outros servios. +Para marcar a doao da coleo de Takeo Hirata para a biblioteca do Masp, dois designers japoneses fizeram ontem uma palestra na Fiesp. +Takuo Hirano e Tetsuyuki Hirano, pai e filho, vieram falar sobre o novo conceito de design que esto desenvolvendo. +Atravs da sua empresa, a Hirano Design International Inc., que tem sede no Japo e filial em Chicago, eles promovem o casamento entre o design e a administrao de empresas. +Eu estou apaixonado, declarou o presidente Itamar Franco, 63, no hall do Teatro Nacional de Braslia, onde assistiu anteontem o bal Quebra Nozes com a pedagoga June Drummond, 31. +June no respondeu se tambm estava apaixonada. +Apenas sorriu. +Itamar disse que o assdio da imprensa continua atrapalhando muito seu namoro com June. +Com cinco livros publicados, e um sexto em preparao, o escritor aguarda apenas, para a definitiva consagrao, que Hollywood consiga adaptar alguma de suas histrias malucas para as telas. +Strip-tease, que a Companhia das Letras lana hoje no Brasil, o ltimo romance de Hiaasen. +A mesma editora publicou, em 93, o livro de estria do escritor, Caa aos Turistas. +O ciclista espanhol, 48, se suicidou em Caupenne d'Armagnac, no sul da Frana com um tiro. +Em 1973, Ocaa venceu a Volta da Frana, a maior competio do gnero no mundo. +Piotr Wator, 21, do Gajowianka, da 3 diviso, morreu domingo durante partida, em Cracovia. +Wator se chocou com um companheiro de equipe. +Cerca de 80 % da rea do municpio considerada de preservao ambiental. +O aumento na arrecadao possibilitou que, depois de 30 anos, a prefeitura pudesse comprar trs caminhes, um nibus e um trator, disse o prefeito Fausto Camper, 43 (PMDB). +E a Regina Cas me disse que churrasco da Brahma, vulgo churrasquinho de Los Gatos, assim: voc enfia o dedo na picanha e fica rodando. +Rarar. +Dedo na brasa! +E sabe quem eu ainda no vi? +O Chato Mesquita do Pernil. +O Jabaury Jr. a gente encontra toda hora. +Esse t trabalhando. +T cobrindo at vaca no pasto! +Rarar! +No caso do carto Unidas, o possuidor tem desconto de at 5 % na locao, possui tratamento preferencial e melhoria na categoria de carro. +O carto TVA vai permitir a seus portadores, a partir deste ms, a participao nas primeiras exibies realizadas em espaos culturais do Banco Nacional. +A American Express estuda tambm lanar cartes de afinidade. +Os cartes Sollo j realizaram contratos, entre outros, com o Jockey Club de So Paulo, a Fundao Getlio Vargas, a Associao Paulista de Propaganda, a Birello e a Jog. +Moradora da Mooca, nos ltimos cinco anos Regina virou o ano com Hilton, o namorado, no Recanto da Cachoeira, uma estncia no municpio de Socorro (SP). +Brigada com o namorado, Regina optou por estrear na So Silvestre. + Foi timo, voc fez um minuto melhor do que o esperado, disse a Medeiros ao cruzar a linha de chegada. +Ganhou um abrao, um beijo no rosto, e foi para a Mooca estourar uma champanhe meia-noite. +O sindicalista Luiz Antnio de Medeiros correu a So Silvestre em busca de resultados. +E chegou l. +Com uma ponte de safena e uma mamria implantadas h um ano e meio, fez o trajeto em 1h41 58 minutos atrs do vencedor. +Na chegada do ano em que disputar uma vaga no Senado pelo PP-SP, deu mais de duas dezenas de entrevistas, posou para fotos, e deixou 1993 nas emissoras de rdio e TV. +As chibatadas rompem a pele e as cicatrizes ficam por toda a vida. +A dor faz os golpeados desmaiarem. +Ao ouvir a negativa a seu recurso, Fay no demonstrou reao. +Em Cingapura, a pena de chibatadas obrigatria para tentativa de homicdio, roubo, estupro, trfico de drogas e vandalismo. +Na discusso com a bancada, FHC deve tratar da reviso constitucional e da delegao que recebeu do partido para negociar as alianas eleitorais. +A reunio da Executiva do PMDB deve ser palco de mais um embate entre os adeptos da candidatura do ex-governador Orestes Qurcia e os antiquercistas. +No PPR, a discusso ser sobre a poltica de alianas e se ela inclui o PSDB, alm dos novos candidatos do partido a presidente. +O mais cotado o senador Esperidio Amin (SC). +Apesar da entrada da frente fria no Estado, so poucas as chances de chuvas no interior, regio mais afetada pela falta de chuvas. +Mas o abrandamento da inverso trmica (fenmeno causado por massas de ar quente que impedem a disperso dos poluentes) e a volta dos ventos devero contribuir para melhora da qualidade do ar. +FHC ressalvou que no conhecia a proposta com detalhes e que o governo est aberto a negociaes. +Entretanto, disse no saber que perdas so essas, e que at agora todo mundo est ganhando com a URV. +Pelo que a Folha apurou, a equipe econmica no est disposta a aceitar modificaes nos dispositivos sobre salrios da medida provisria que instituiu a URV. +No podemos aceitar medidas que contrariem o combate inflao, disse FHC, ao responder sobre o assunto. +Falando aos membros da Comisso de Assuntos Econmicos do Senado, o ministro disse estar feliz por no haver, at agora, sentenas judiciais contrrias s medidas do plano econmico. +Ele disse que a Justia do Trabalho em So Paulo havia decidido que no havia perdas salariais para os metalrgicos do Estado. +Ao contrrio do que tem acontecido em suas ltimas declaraes pblicas, FHC evitou ontem fazer ataques aos chamados aumentos abusivos de preos por parte dos oligoplios grandes empresas que dominam determinados segmentos da economia. +O ministro limitou-se a dizer que havia conversado por telefone com assessores do presidente Itamar Franco sobre a proposta de uma nova legislao contra abusos do poder econmico. +Esse prazo, no Brasil, muito flexvel. +O Opala durou 23 anos, o Chevette, 20, a Kombi segue firme desde 1957. +As redes de revendedores das duas marcas festejam a dissoluo. +Os distribuidores Ford esto exultantes. +Foram recebidos na semana passada em Detroit por Alex Trotman, principal executivo da Ford norte-americana. +Assim como a China, aceleramos a reforma no campo, comeamos por a. +Todos sabem que as reformas na agricultura foram a locomotiva que levou a China a seu estado atual. +Por isso, j em 1991, comeamos a introduzir a propriedade privada da terra, desmontando o sistema socialista. +Hoje ns temos o maior setor agrcola privado entre os pases da Comunidade de Estados Independentes (aliana que substituiu a URSS). +Por enquanto, s esto definidos investimentos da ordem de US$ 600 milhes nos prximos trs anos na Argentina e mais US$ 500 milhes no Mxico, anunciou Douglas Ivester. +Nos ltimos cinco anos, s na Argentina, a Coca-Cola investiu US$ 800 milhes na aquisio de novos equipamentos e desenvolvimento de novas tecnologias, lembrou Ivester aps encontro com o presidente argentino Carlos Menem. +Uma das consequncias disso outra aberrao na sade brasileira: em 1991, os hospitais psiquitricos cadastrados rede federal consumiam 7,5% das despesas com internaes realizadas pelo SUS e representavam o equivalente a 18% dos leitos. +Esses nmeros vo de encontro com a tendncia mundial, segundo a qual o paciente deve ficar o menor tempo possvel internado, e os esforos devem ser para reintegr-lo na sociedade. +Apesar de seu discurso duro contra eventuais aumentos de preos, Ciro admitiu, pela primeira vez, rever a estratgia de reduo das alquotas de importao. +Ele disse aos empresrios -- que mantiveram suas crticas poltica de abertura comercial indiscriminada -- que o governo pode aumentar de novo as alquotas de importao em todos os setores. +Os juros esto em alta. +O rendimento projetado das cadernetas saiu de um patamar de 42% nos depsitos na semana passada para 46% nas aplicaes nos prximos dias. +Francisco Lafayette, administrador do Banco Banorte, diz as cadernetas esto com rendimento atraente, mas que os fundos de commodities do a vantagem adicional do resgate dirio com rentabilidade a partir da carncia de 30 dias. +Cada modelo de fac-smile, com suas especificaes tcnicas peculiares, recebe uma patente do tipo MI (modelo industrial). +No um PI porque s funciona com algo que j existia (o telefone). +O registro do tipo DI (desenho industrial) diz respeito configurao e cor do produto. +O Coprotest uma verso revista e melhorada das anti-higinicas e pouco seguras latinhas para coleta de fezes para exames parasitolgicos. +O produto no tem similares em todo o mundo e est sendo muito bem aceito nos Estados Unidos, afirma Adolfo Moruzzi, 49, diretor da NL. +A praa deveria ser reconstruda aps a construo do Piscino. +A praa, por ser tombada, deveria ficar como era antes da obra. +Segundo o sindicato dos arquitetos, o projeto da prefeitura modifica o desenho original. +Entre as modificaes, est a construo de um anfiteatro ao ar livre. +O sindicato fundamenta o pedido de embargo da obra com a omisso da prefeitura, que no submeteu o projeto de reurbanizao da praa ao Condephaat. +A assessoria de imprensa da Secretaria de Vias Pblicas disse ontem que a prefeitura s vai se manifestar depois que for notificada pela Justia. +Jason o maior assassino de todos, com 126 vtimas. +Myers vem a seguir, com 46. +Freddy est na rabeira, com 37. +Coincidentemente, Jason tambm levou o maior nmero de balas (132), contra 27 de Michael Myers e apenas seis de Freddy Krueger. +Os avais so respeitveis, mas no bastam. +Tem que se demonstrar atravs de contas e de raciocnios que o expurgo significar perda. +Como no se pretende o monoplio da verdade, apresento o raciocnio (e as contas) que fundamentam a tese de que no haver perdas. +Quem pensa o contrrio, que trate de comprovar que o raciocnio est furado. +Ao desembarcar no Canad, troque a moeda norte-americana por dlares canadenses. +No esquea dos culos escuros e uma capa de chuva. +Existem outras formas de burlar o reajuste anual. +Alguns proprietrios fazem um contrato para o apartamento e um acordo, muitas vezes verbal, para a locao da garagem e/ou do telefone. +Nesse caso, embora o valor do aluguel do imvel fique fixo por 12 meses, os da garagem e do telefone podem ser reajustados at mesmo mensalmente. +H ainda aqueles que optam pela locao de temporada. +Ou seja, alugam o imvel por um prazo mximo de 90 dias e ainda podem receber o valor adiantado. +A equipe do ministro da Fazenda, Rubens Ricupero, avalia que o processo de crescimento do volume de dinheiro em circulao, decorrente da queda da inflao, est praticamente encerrado. +Segundo o diretor de Assuntos Internacionais do Banco Central, Gustavo Franco, as emisses de reais cresceram bastante na primeira semana de julho, mas esto estveis desde ento. +Blatter disse que o Comit no levou em considerao o fato do jogador ter se desculpado e comparecido ao hospital para visitar o norte-americano Tab Ramos. +Tambm no nos interessa se o jogador estava internado ou no. +O desenlace, porm, no veio na forma do golpe de timo com reforma radical, mas foi precipitado pelas duas balas que tiraram a vida de Colosio. +Mas o resultado, surpreendentemente, foi o mesmo no que se refere reconstituio do princpio da autoridade. +Uma hora aps a morte de Colosio -- na realidade minutos depois do pblico receber a trgica notcia, Octavio Paz pedia um basta aos excessos verbais e ideolgicos de alguns intelectuais e jornalistas e s numerosas e irresponsveis apologias da violncia. +Cantora rejeita som 'enlatado'. +A cantora e violinista Meca Vargas no faz coro com os contentes com o computador. +Para ela, a mquina produz msica enlatada, desprovida de fora viva. +Os paulistanos esto pagando menos tambm pelos produtos da cesta bsica. +Outra pesquisa do Datafolha mostra que estes alimentos recuaram 0,47% na ltima semana de novembro. +Mesma tendncia teve o custo da cesta bsica pesquisado pelo Procon, em convnio com o Dieese. +Este levantamento de preos que inclui tambm produtos de higiene e de limpeza mostra recuo de 1,58% em relao sexta-feira anterior. +Estamos na transio. +Desculpe-se. +Agosto ser outro mundo. +Mais perto da eleio. +O governo no est errado em formular regras que possibilitem o alongamento. +No Brasil, coisa esquisita, j se negociou CDB de seis meses uma eternidade, hoje em dia. +A droga salmeterol mais eficaz para tratar asma a longo prazo do que a comumente usada, segundo estudo na ltima edio da revista da Associao Mdica Norte-americana (Jama). +A droga pode ser inalada atravs de bombinhas de aerossol. +Ela teve ao prolongada como dilatadora dos brnquios (tubo por onde o ar passa aos pulmes). +O alemo no teve muita chance de lutar pela pole. +Aps fazer uma nica volta rpida, Schumacher emprestou seu carro para o companheiro Jos Verstappen, que havia rodado nos treinos da manh. +O piloto holands, em sua primeira volta, perdeu o controle do Benetton e foi parar na brita. +Final de treino para ele e Schumacher. +Em relao aos remdios, houve um aumento abusivo, perto dos 60% em janeiro. +Dallari, aps negociaes com os laboratrios, candidamente anunciou que para janeiro, no se pode fazer mais nada, mas em fevereiro os reajustes sero iguais inflao. +Sequer lhe ocorreu que em fevereiro o excesso de aumento poderia ser compensado. +Distrao, claro. +Mas, em fevereiro, os laboratrios ultrapassaram a inflao outra vez. +Em maro, esboa-se a mesma tendncia. +A Fenasoft deixou de ser um reduto de especialistas em informtica para atrair tambm quem pretendia comprar o primeiro computador. +Estou comprando o micro -- o porttil Aero, da Compaq -- para acelerar as pesquisas da minha tese de mestrado, disse Deborah Caldas. +Enquanto isso (ou entrementes, como nas antigas histrias em quadrinho), o ex-governador ACM descola suntuoso emprstimo para socorrer os cacaueiros da Bahia, quatro anos de carncia, juros de 2% subsidiados, um emprstimo de pai (governo) para filho (fazendeiros). +No fundo, uma indecente doao do dinheiro pblico. +Enquanto isso (outra vez a vontade de escrever entrementes), a Receita Federal divulga a lista dos devedores do Imposto de Renda que nada pagaro. +Mais da metade do rombo na Previdncia tem como causa a sonegao de empresas. +Entrementes (resistir trs vezes quem h de? ), o governo de FHC ameaa reduzir as aposentadorias para cinco mnimos. +O Depav tambm est zoneando o parque. +O trabalho, coordenado pela diretora Vera Bononi, 49, prope solues para o aproveitamento do espao construdo, em especial a marquise. +A Asuapi doou 10 bicicletas para a guarda municipal fazer o policiamento do parque. +Para entrar em ao, a tropa de ciclistas est esperando apenas que acontea o cerimonial de inaugurao, diz Santini. +A guarda usar uniformes especiais. +Segundo Santini, com as bicicletas, o trabalho ser mais rpido e eficiente. +Ele afirma que no tem previso para o incio do trabalho dos ciclistas. +Na disputa pelo mercado de impressoras quem sai ganhando o consumidor. +Vale a pena conferir os produtos e comparar a qualidade de impresso dos modelos jato de tinta. +A Epson mostra na Comdex quatro modelos com tecnologia jato de tinta, que devero substituir os modelos matriciais no dia-a-dia dos usurios e empresas. +O Departamento de Estado dos Estados Unidos ordenou ontem reforos na segurana das representaes diplomticas de Israel no pas para prevenir eventuais ataques terroristas. +Em Nova York, o FBI (Federal Bureau Investigation, a polcia federal dos Estados Unidos) recebeu uma ligao annima no final da noite de anteontem informando que o Consulado de Israel sofreria um atentado a bomba. +O secretrio disse que aguardava apenas uma definio de Paulo Maluf quanto a seu futuro poltico para anunciar medidas de base. +As denncias que vm sendo feitas procedem, disse Raia. +J os quatro leiles realizados durante a Expomilk venderam 124 cabeas de raas holandes, jersei, pardo-suo e girolanda. +O faturamento bruto atingiu R$ 615,7 mil, equivalentes a US$ 721,8 mil. +Uma vaca pardo-suo conseguiu a maior cotao da Expomilk, R$ 36 mil (US$ 42,2 mil). +Foi apresentada pela Agropecuria Amrica e arrematada pela Citrovita Agropecuria. +Folha -- O sr. j fez algum discurso contra a monarquia no Parlamento? +Skinner -- No permitido falar muito contra a monarquia no Parlamento porque supostamente voc deve jurar fidelidade rainha. +A carga tributria das instituies financeiras poder aumentar em 73% caso o Congresso Nacional aprove o aumento da contribuio social sobre o lucro dos bancos e a incluso da obrigatoriedade de recolhimento do PIS (Programa de Integrao Social). +Mesmo assim os bancos continuam entre os setores da economia que recolhem menos tributos. +O ganho de arrecadao com o aumento da carga de impostos dos bancos vai ajudar a fechar em US$ 16,1 bilhes o FSE (Fundo Social de Emergncia). +O governo teve que elevar de 15% para 20% o percentual dos recursos dos tributos que vo fazer parte do FSE para compensar a perda de recursos do Fundo de Participao dos Estados e municpios. +Essa deciso dever dar mais de US$ 12 bilhes ao FSE, s com tributos que pertencem Unio. +O pas vive, neste momento, a parte mais crucial da grande luta pela implantao da cidadania. + quando o rastreamento das pistas da contraveno comea a bater em personalidades acima de qualquer suspeita, dentro do pequeno crculo ntimo da elite cosmopolita brasileira um amlgama que rene intelectuais de esquerda e direita, eleitores de Lula a Maluf, passando por todo o espectro intermedirio, empresrios, profissionais liberais, jornalistas e membros do judicirio. +A corrupo institucional brasileira vai muito alm de ratos gordos como Joo Alves. +Caio Gorentzvaig, cotista da Petroplastic, disse que o arresto cair automaticamente quando seu pai, Boris, se der por citado e oferecer bens imveis penhora. +Prev que isso acontea em dez dias, quando seu pai voltar a So Paulo. +O tucano tem passado a maior parte do tempo em seu quarto e, ao contrrio dos 60 hspedes, no tem feito exerccios. +Mesmo assim, o governador eleito tem seguido a dieta hipocalrica da clnica, onde a ingesto diria de 350 calorias. + boa, mas pouca. +Apesar de ter dito h dias que entre o justo e legtimo e o legal, opta pelo justo e legtimo, Lula disse que a reforma agrria, em sua eventual governo, ser feito dentro da lei. +Lula fica na frica do Sul at quarta-feira noite, quando viaja ento para a Alemanha, onde permanece at sexta-feira noite. +Disaster Movie, de 1979, foi uma revelao: apesar da produo apertada, esse curta-metragem revelava um talento mais que incontestvel. + um pouco em funo disso que Anjos da Noite decepciona. +A incurso de Wilson Barros ao universo dos seres noturnos paulistanos revela um trao tipicamente brasileiro: uma espcie de vergonha de ser brasileiro. +Talvez por isso a So Paulo de Anjos da Noite parea com qualquer lugar do mundo. +O talento de Barros continua l, apesar de Marilia Pra se empenhar em tomar conta do filme com um histrionismo fora de lugar. +Mas de alguma forma representa a tendncia formalista que marcou o cinema brasileiro na segunda metade dos anos 80. +Alm de Serpa e Teixeira, j estavam sob investigao judicial eleitoral os candidatos Mrcia Cibilis Viana (federal, PDT), Paulo de Almeida (federal, PSD) e Aluizio de Castro (estadual, PPR). +Ontem, o plenrio do TRE do Rio determinou a recontagem dos votos da 81 seo da 77 zona. +O tribunal anulou 108 votos de Nilton Cerqueira (federal, PP) e 93 de Emir Larangeira (estadual, PFL) na 113 zona (Niteri). +Os votos foram preenchidos com a mesma letra. +Tambm foram anuladas duas urnas em Barra Mansa. +A Reebock teve resultado melhor do que o esperado no segundo trimestre. +O lucro cresceu 24% para US$ 51 milhes e as vendas, 18%, para US$ 776 milhes. +A Nike tambm teve bons resultados, pondo fim a especulaes de que teria passado o auge das famosas fabricantes de tnis dos EUA. +O hotel Deville, perto do aeroporto de Guarulhos, diz que, alm de executivos, tem hospedado muitos turistas, principalmente com destino ao Caribe e Disney. +Afirma que, na primeira quinzena de julho, o ndice mdio de ocupao foi de 80%. +Para elaborar a lista, o grupo mede o custo de vida em quase 200 lugares. +So analisados os preos de 155 produtos e servios. +Atrs de Buenos Aires, esto duas cidades asiticas (Hong Kong e Taip) e duas europias (Zurique e Genebra, ambas na Sua). +As declaraes foram recebidas por Maradona como uma tacada de beisebol na cabea, como o prprio meia definiu. +Maradona negou veementemente as crticas da me de Franco. +No sei o que dizer. +Estou surpreso, foi o que Franco disse a sobre as declaraes da me. +A resoluo normativa de 4 de junho de 1987 do Instituto Brasileiro de Turismo (Embratur) afirma que os meios de hospedagem de turismo dos tipos hotel, hotel de lazer e hotel-residncia, de qualquer categoria, ficam obrigados a dispor de local especfico, devidamente sinalizado e apropriado ao embarque e desembarque de usurios portadores de deficincias, alm de estacionamentos para seus veculos. +A lei diz ainda que 2% dos apartamentos tm que ser adaptados e devem haver facilidades para acesso e circulao em todas as dependncias sociais do hotel. +As rodas de liga de alumnio ou magnsio tm manuteno menos frequente. +A Secretaria da Fazenda do Estado de So Paulo determinou que, quando um contribuinte realizar venda de mercadorias em feiras, exposies ou em locais chamados de outlets ou feira de promoes e permanecer na rea determinada por mais de 60 dias, ser obrigatria a inscrio do referido local no cadastro de contribuintes do ICMS (Fund. portaria CAT 116/93). +A restituio do IRPF poder ser feita a terceiros, desde que: se de valor at 80 Ufir, mediante simples autorizao por escrito do beneficirio, acompanhada de cdula de identidade e CPF do representante e do representado, para verificao das assinaturas; e se de valor acima de 80 Ufir s poder ser paga a procurador (Fund. instruo normativa DRF n. 38/92). +Segundo ele, o que retardou o trabalho da equipe foram as contuses de alguns jogadores importantes, como Marcelo Negro e Giovane. +J pensando no Mundial da Grcia, que ser disputado em setembro em Atenas, o tcnico brasileiro retoma o comando dos treinos da equipe a partir da prxima segunda-feira. +Correo: hoje trazemos as respostas das palavras cruzadas de sbado. +A resposta para as palavras cruzadas de hoje foram publicadas ontem. +Segundo Rosa, nutricionista e professora da PUC de Campinas, junto com os alimentos se ingerem e digerem afetos. +Isto : os diversos tipos de refeies so investidos de smbolos que alteram o valor que as pessoas atribuem ao que comem. +Para chegar a suas concluses, Rosa entrevistou um grupo de funcionrios pblicos (tcnicos administrativos) que trabalham e comem no centro velho da cidade de So Paulo (ruas Lbero Badar, So Bento e adjacncias). +Observou tambm os lugares onde essas pessoas almoam. +VITRIA Josias de Souza ensinou sexta-feira, nesta Folha, que pesquisa no urna. +Eu tambm acho, mas preciso avisar o pessoal do PT. +Nunca vi um partido render-se to rpida e incondicionalmente como o PT est fazendo. + notvel o baixo astral da militncia, de quadros dirigentes e de simpatizantes em geral. +De acordo com o amassado, sem que seja preciso interferir na pintura. +Agora, as negociaes so internacionais: Amlia vai conversar com a Krupp alem e a Japan Steel. +Cada obra consome 1,5 tonelada de ao (US$ 6 mil) mais US$ 10 mil de mo-de-obra. +Multiplique isso por 15, some o valor do transporte e se tem US$ 400 mil. +Sete Ondas no est partindo do marco zero. +Amlia j tem o OK inicial de trs pases: Alemanha, Frana e EUA. +Na Alemanha, a obra ficaria na Casa da Cultura dos Povos em Berlim. +O Centro Internacional de Arte e Escultura, de Vaison-la-Romaine, sul da Frana, tambm tem interesse pela obra. +Nos EUA, o Museu da OEA (Organizao dos Estados Americanos) j tem agendada uma exposio de Amlia, mas ela no sabe se instala l suas Sete Ondas. +A negociao com patrocinadores o processo mais complexo, segundo a artista. +As esculturas, ela diz que faz em seis meses. +Nos perodos de choques, 41% dos contratos do setor de confeces precisaram ser renegociados entre compradores e vendedores. +Nas pocas de normalidade da economia inflacionria brasileira, esse percentual ainda chega a 7,5%, contra 2,3% do Chile, pas que j alcanou sua estabilizao. +Duas pessoas morreram em um acidente ocorrido na noite de ontem no km 2 da rodovia Joo do Amaral Gurgel, que liga Caapava a Jambeiro, no interior de So Paulo. +Um Gol e um Voyage se chocaram. +Eles viajavam em sentido contrrio. +O motorista do Gol e uma passageira do Voyage morreram na hora. +A polcia ainda no sabe qual foi a causa do acidente. +O diretor de Assuntos Internacionais do Banco Central, Gustavo Franco, disse que o BC vai comprar dlares por uma cotao bem abaixo de R$ 1,00, preo pelo qual vende a moeda norte-americana. +Ontem, no mercado privado, sem interferncia do Banco Central, o dlar foi negociado a R$ 0,90. +Segundo Franco, o BC vai comprar dlares quando a cotao cair a um ponto determinado, no conhecido pelo mercado. +O povo, assim, mais destinatrio de um discurso poltico, que ele mesmo no profere, quando muito escuta, que seu emissor ou senhor. +Por isso, estando o demos na recepo e no na produo do discurso poltico, melhor dizer regime demtico que democrtico. +O kratos no do povo; seu, se tanto, o ouvido, o olhar. +Mas, nesta sociedade, cabe papel relevante ao conforto. +Os discursos da frugalidade, da austeridade ficaram para as repblicas gregas e romanas, aquelas que Montesquieu ainda celebrava, em meados do sculo 18, quando dizia que o regime republicano se caracteriza pela disposio de seus cidados a renunciar ao bem privado em favor do bem comum e por isso, acrescentava ele, trata-se de regime impossvel na modernidade, na qual se busca a vantagem pessoal. +Segundo o jogador, o Palmeiras s ter dificuldades para garantir o ttulo na partida contra o Corinthians pois deve enfrentar antes adversrios teoricamente fceis (Mogi Mirim, Ituano e Santo Andr). +Em o ltimo jogo, ns teremos um prazer, ou de sermos campees ou de dar o ttulo ao So Paulo, garantiu Moacir. +Nosso objetivo no campeonato, portanto, ser definido depois do domingo, quando enfrentamos o So Paulo, disse o volante. +A entrada de capitais, para ser esterilizada monetariamente, teve de ser absorvida pelo crescimento da dvida pblica, o que por sua vez requereu um supervit fiscal primrio crescente. +h mais de um ano, o Mxico encontra-se numa situao de desequilbrio potencial permanente no balano de transaes correntes e no oramento fiscal, que foi compensado mais recentemente por um endividamento crescente do setor pblico nas praas financeiras internacionais, em particular na Bolsa de Nova York. +A quinta rodada da Superliga Nacional Masculina de Vlei da temporada 94/95 prossegue hoje com mais quatro jogos. +O Palmeiras/Parmalat que est em terceiro lugar na classificao geral, tenta defender sua invencibilidade no torneio diante do Flamengo, no Rio. +Na tese, despreza o papel do povo num governo. +Mas, nos anos 90, os yuppies desenvolvem conscincia. +Joe Pesci vai ser a conscincia em carne-e-osso. +O ator vive um sem-teto, que o acaso coloca diante do estudante. +A TV por assinatura no Brasil est nascendo concentrada nas mos de quatro grupos: Organizaes Globo, Multicanal (subsidiria da Companhia de Minerao do Amap), Rede RBS e Abril. +E abre s para almoo e drinks ao cair da tarde. +Apesar de ningum l ser mais amigo do rei, a turma continua na ativa. +Os policiais foram a uma casa, indicada pelas duas mulheres, onde estariam outros integrantes da quadrilha, mas nada foi encontrado ali. +O Parque So Carlos habitado por moradores de renda baixa. +A casa em que Faria ficou era de dois quartos. +O quarto em que ficou era pequeno e sua porta permaneceu sempre aberta. +O conjunto fica prximo estrada de Madureira, uma das principais vias de Nova Iguau. +Segundo a polcia, a estrada procurada por grupos de extermnio para desova de cadveres. +Esta a simplicidade que mata, quando mal traduzida em simploriedade. +Mas Michael Tilson Thomas, regendo tudo com uma segurana natural e nenhuma pompa, parece o regente por excelncia desse nosso tempo, que ao menos na msica um tempo de refinamento e inteligncia, alm de comunicao. +Simptico, fluente (com as palavras tambm), vontade no papel de regente jovem (embora beirando os 50 anos), no h um nome melhor para dirigir a New World Symphony, uma orquestra de bolsistas. +Nem simples, nem muito menos beethoveniano o Concerto para Violino de Tchaikovski. + uma obra infelizmente popular. +Est longe de ser o melhor de Tchaikovski, mas o diabolismo da parte solista no deixa nunca de impressionar. +Diabolismo no falta ao solista Robert McDuffie: um violinista de mil dedos, afinadssimo e sem medo da msica. +Mas toca tambm com certa brutalidade, impacincia ou agressividade com a msica. + um msico atltico, no limite extremo da escola americana de interpretao. +Os juros despencaram ontem com a expectativa otimista para a inflao de dezembro. +As taxas de juros equivalentes dos CDBs diminuram 2,31 pontos percentuais em relao mdia da ltima segunda-feira. +Em 1920, a sujeio do sulto Mohamed 6 ao Tratado de Svres desagradou os nacionalistas liderados por Mustaf Kemal, militar que lutou nas guerras balcnicas e na Primeira Guerra Mundial. +J em Ancara que se tornaria capital Kemal presidiu a grande Assemblia Nacional. +Eleito presidente e primeiro-ministro, ele cuidou pessoalmente das operaes militares durante as guerras grego-turcas (1920-22). +Depois do tratado de paz de 1923, aboliu o califato, a poligamia, substituiu prticas legais islmicas por outras de inspirao europia, adotou o alfabeto romano e o calendrio gregoriano e concedeu s mulheres direito de voto. +O assalto foi impedido pela segurana do carro blindado. +Houve tiroteio. +O carro-forte do Banespa estava estacionado no ptio interno quando um caminho basculante. com homens armados na traseira. bloqueou o porto. +O turismo de pesca ainda atrai 75 % dos visitantes que vo ao Mato Grosso do Sul. +Muitos hotis, no entanto, perceberam o crescente interesse pelo ecoturismo e diversificaram suas atraes. +Promovem safris fotogrficos, focagem noturna de jacars e outros passeios. +Cobram dirias a partir de US$ 50 (CR$ 22,9 mil) por pessoa, com penso completa. +H opes mais econmicas, como os campings, por US$ 5 (CR$ 2.200,00) por pessoa. +Aguap (067-241-2889) -- A 59 km de Aquidauana, a fazenda tem 7.000 hectares, campo de pouso e sete apartamentos. +Os pacotes, com penso completa, incluindo passeios, vo de US$ 150 (CR$ 68,8 mil), dois dias, a US$ 300 (CR$ 137,7 mil), cinco dias. +O pacote de pescaria, com barco, motor, piloteiro e penso completa, 20% mais caro. +No camping, a diria de US$ 5 (CR$ 2.200,00). +Nessa obra literalmente seminal, Miller estabeleceu o padro do que seria dali para diante a sua literatura: memrias pessoais vistas com a mincia de um estudo biolgico e narradas no tom exaltado de um profeta bbado. +A matria-prima desse primeiro livro o perodo que o autor passou em Paris nos anos 20, perambulando de prostbulo em bar, de quarto emprestado a banco de estao, enquanto tentava escrever. +O otimismo de Van Himst se justifica, em parte, pela entrada do centroavante belgo-croata Josip Weber no time. +Ele marcou cinco gols na vitria sobre a Zmbia, no comeo do ms. +Foi o mais expressivo resultado j atingido por uma seleo belga na histria. +Weber deu maior profundidade equipe. +Encontramos um homem-gol de que sentamos falta h muitos anos, disse Van Himst, ele prprio um dos maiores jogadores que a Blgica j teve. +Carmelita Aralse Tebele, 89, moradora do oitavo andar, passou mal com a fumaa e teve de ser carregada pelas escadas at uma ambulncia. +Beth Tebele, 62, filha de Carmelita, foi medicada junto com a me. +O comerciante Albano Miguel Ferreira, 37, disse que estava na rea de servio de seu apartamento, no primeiro andar, e viu o fogo comear em um barraco nos fundos da loja. +No caso dos microcomputadores, o aumento foi de 200 unidades para 500 unidades mensais, no mesmo perodo. +H uma busca frentica por eletrnicos que facilitem o dia-a-dia do cidado em casa e no escritrio, afirma Quintana. +Andrade afirmou que a limpeza feita diariamente. +No largo Santa Ceclia, funcionrios da Administrao Regional recolheram material acumulado pelos indigentes e desinfetaram bueiros. +Nos primeiros 20 dias deste ms foram assassinados no Rio 223 pessoas mdia de 11 por dia. +O ndice o mesmo registrado em novembro do ano passado. +O diretor da DRFVAT, Leonilson Ribeiro, disse que a presena de militares e policiais nas ruas causou a diminuio do nmero de carros roubados e furtados. +Muitas vezes em vos quase sem escalas, algumas espcies voam mais de 10 mil km, do rtico at a lagoa. + o caso de um maarico-de-papo-vermelho, anilhado na lagoa do Peixe e observado 13 dias depois no Estado de Nova Jersey (nordeste dos EUA). +Apesar de ser a primeira rea no Brasil a ser includa, em 1991, rede hemisfrica de reservas para aves migratrias na categoria de reserva internacional, o Parque Nacional da Lagoa do Peixe, criado em 1986, no existe de fato, pois ainda no h um plano de manejo e de desenvolvimento. +O aumento da cesta bsica na cidade de So Paulo na terceira semana de junho foi de 10,27% em cruzeiros reais, segundo pesquisa do Procon em convnio com a Fipe. +No acumulado do ms atinge 43,85% , superando as variaes de maro (40,91%), abril (40,23%) e maio (40,23%). +Em 94 acumula alta de 701,5%. +Entre as maiores variaes da semana esto a sardinha em lata (30,52%) e papel higinico (28,18%). +J as maiores quedas, dos preos mdios, ficaram com a cenoura (- 13,82), batata comum (5,14%) e po francs (- 1,25%). +O grupo alimentao, porm, vem tendo as maiores altas do ms. +J o Regency St. John Resorts ter amplida sua rea esportiva e aprimorado seu servio de hspedes. +O Hyatt Regency Aruba tambm ter melhorias em seu cassino e a instalao de duas piscinas de hidromassagem. +Essas dificuldades, que surgiram desde o lanamento da idia, estimularam economistas e governo a buscar caminhos mais rpidos. + o caso da segunda opo, uma verso mais apressada da primeira hiptese. +O governo cria a URV e estabelece um prazo de trs a quatro meses para que a sociedade negocie converses voluntrias. +E anuncia, no momento mesmo da criao do indexador, que a partir de determinada data a converso ser obrigatria, conforme regras predeterminadas. +Dando um tempo para os acertos, FHC cumpre a promessa de que ser tudo voluntrio. +Mas na segunda fase, a converso obrigatria exige regras de converso, tablita e possivelmente um congelamento para impedir especulao na nova moeda. +E isso um choque com data marcada. +Mas uma vantagem deixar todos os passos definidos desde o momento de lanamento do plano. +O Banco Mercantil de So Paulo realizou lucro lquido de CR$ 52,3 bilhes no primeiro semestre. +O que corresponde a 3,65% sobre o patrimnio lquido. +O lucro lquido do Banco Bandeirantes no primeiro semestre foi de CR$ 44 bilhes, com um retorno de 12,6% sobre o patrimnio lquido. +Teve direito at rara presena de Maria Bethnia na platia a ltima performance carioca de Gilberto Gil e Caetano Veloso. +Junto com sua Magda Collares, Fausto Silva decola dia 13 para temporada de trivial variado cultura e showbizz em Nova York. +Na quarta-feira, o So Paulo tomou um vareio do Palmeiras. +Se o jogo terminasse 6 a 0 no primeiro tempo no seria mais que o fiel espelho da partida. +Parte porque o Palmeiras jogou com extremo empenho e raro talento. +Parte porque o tricolor entrou em campo mal escalado por Tel, que tentou consertar logo aps, com a entrada de Vtor na lateral e a passagem de Cafu para o lugar de Jamelli. +Por fim, pela inexpressiva atuao das duas maiores estrelas tricolores Leonardo e Cafu. +Esta tarde, por fora da suspenso de Axel, j comea melhor no papel, com Vtor na lateral e Cafu no meio. +Mas hora de Tel dar um toque de classe nesse meio-campo com Vlber e voltar com Palhinha mais frente, que esse jogo vale o campeonato. +Em algumas cidades, onde os pais tm poder de deciso no sistema educacional, os projetos de novas escolas incorporam computadores, CD-ROM, modem e linhas telefnicas para permitir que os estudantes se comuniquem on line. +Americano exagerado. +Costuma encarar a tecnologia como a soluo milagrosa para todos os problemas. +Computador na sala de aula no salva ningum da ignorncia. +Mas pode facilitar a vida dos estudantes. +Traz dicas de produo grfica, como alinhamento. +Tem vrios apndices de software para DOS ou Windows. +A venda em bancas de jornal ou pelo telefone (0800) 11-5353. +Voltado para usurios leigos e avanados, os 13 captulos do dicas para aproveitar melhor os recursos do sistema operacional da Microsoft. +Inclui disquete de atualizao do 6.0 para 6.2. +Preo: CR$ 16,9 mil. +Tome-se o caso de Ventersdorp, 200 km a oeste de Johannesburgo, uma das 14 cidades declaradas pelo governo reas de turbulncia, estgio suave do estado de emergncia. +Em Ventersdorp, a nova e a velha frica do Sul so vizinhas. +Na delegacia de polcia, a nova bandeira de seis cores est hasteada no mastro e o juiz David Postman acabou de concluir a contagem dos votos. +Creio que essa vocao de poeta determinou um dos maiores problemas (h vrios) na realizao da obra de Murilo Mendes. +H, com efeito, um poetismo em seus poemas. +Murilo sempre est partindo do pressuposto de que ele poeta. +Afirma-o, de maneira explcita, em sua obra. +O resultado muitas vezes ingnuo, infantil. +A CompUSA de Los Angeles vendia, na semana passada, a impressora jato de tinta Epson Stylus 800 por US$ 199. +Na loja de Nova York, o Compaq Aero est por US$ 996,97 (se chegar s 9h da manh, ganha o cupom de desconto de US$ 100). +No aceita carto de crdito internacional. +A J&R Computer World, apesar de meio fora de mo, tem preos competitivos. +A equipe de Maluf no a primeira a deixar de lado o problema de enchentes. +A prefeita Luiza Erundina priorizou o transporte pblico. +Jnio Quadros tambm gastou em obras virias e Mario Covas marcou sua gesto pela construo de corredores de nibus exclusivos, como o da avenida Nove de Julho. +Ontem a cidade conferiu os estragos causados pela chuva de quinta feira, a maior chuva de maro desde 1930. +Reynaldo de Barros, secretrio de Vias Pblicas, participou de uma cerimnia pela manh e depois no pde mais ser encontrado em seu gabinete. +O Banco Mundial, o FMI e o Gatt so instituies pblicas que deveriam ter suas polticas submetidas a esferas pblicas de deciso e ter a transparncia necessria para serem avaliadas e monitoradas pelos cidados. +Recentemente, o Banco Mundial constituiu um grupo independente de inspeo para analisar queixas sobre seus projetos, iniciou uma nova poltica de informaes e o Gatt organizou o primeiro seminrio com a participao de ONGs. +A Secretaria da Sade de Goinia recebeu ontem CR$ 163,7 milhes, atravs de convnio assinado com a Fundao Nacional de Sade. +Desse total, CR$ 127 milhes sero destinados campanha de combate dengue e ao pagamento dos 200 agentes de sade contratados para a campanha. +Gois ter dentro de dois anos o seu Zoneamento Ecolgico-Econmico. +O superintendente de Meio Ambiente, Clarismino Jnior, disse que um dos objetivos identificar as macrozonas existentes no Estado. +Segundo ele, a proposta inicial zonear no primeiro ano a Bacia do rio Meia Ponte. +As faixas, listras e crculos desenhados nos gramados norte-americanos da Copa no so apenas um recurso esttico. +Eles servem para camuflar o improviso dos estdios da Copa. +Os nove estdios do Mundial so normalmente usados para o futebol americano, que tem marcas prprias na grama. +Comentei com minha amiga espanhola, Dolores de Pana, a profuso de beijos melados que massacraram minhas aveludadas bochechas na Bienal. +Ela subiu nos tamancos flamencos. + por essas e outras que a gente acaba pegando uma micose na chica e depois no sabe explicar por qu, bradou a espanholita. +E props que eu desencadeasse nesta coluna uma campanha nacional contra saudaes lambuzadas. +Ta, gostei da idia! +Al, vtimas do abuso oscular do Brasil! +Faam como eu! +Em sinal de protesto, de hoje em diante, quem me cumprimentar com beijo suado leva uma muqueta no focinho. +Viva Howard Hughes! +O IBGE divulgou tambm ontem a inflao registrada no Rio e So Paulo entre os dias 9 de maro e 7 de abril. +O IPC-amplo (cesta de consumo de famlias com renda de at quarenta salrios mnimos) registrou 42,65% em So Paulo e 41,96% no Rio. +Esses ndices revelam queda de inflao de 0,23 ponto percentual em So Paulo e 0,59 ponto percentual no Rio, em comparao com o perodo anterior. +O candidato do PMDB chegou a Bauru s 10h de ontem, num jatinho fretado pelo partido. +Foi recebido no aeroporto por militantes, prefeitos e vereadores. +Qurcia disse que tem sido vtima de uma campanha srdida com o objetivo de denegrir sua campanha. +O modo de escapar disse simples: aplica-se um reajuste de, digamos, 40% ; em seguida, avalia-se qual a inflao diria, digamos de 1,5% , e a se aplica ao preo cheio um desconto decrescente. +No primeiro dia aps o reajuste de 40% , o desconto de 38,5% ; no segundo de 37% e assim por diante. +Assim, um carro de CR$ 10 milhes, sairia no primeiro dia aps o reajuste por CR$ 6,15 milhes. +No segundo dia, por CR$ 6,3 milhes e assim sucessivamente. + uma URV s avessas. +A generalizao dessa indexao diria no s acostuma a economia, como passa a exercer presso para que o governo oficialize e universalize a prtica. +O assunto veio tona durante conversa do advogado Osmar (Nuno Leal Maia) com o jardineiro Kennedy (Alexandre Moreno), jovem negro que sofrera insultos racistas do patro, o empresrio Raul Pelegrini (Tarcsio Meira). +Uma semana depois, trs organizaes paulistas resolveram contestar a informao na Justia Federal. +Os termos da reforma foram assinados quarta-feira pelo prefeito Paulo Maluf, pelo abade Dom Isidoro Oliveira Preto (mosteiro) e por representantes do Banco de Boston, Philips e Akzo/Tintas Ypiranga, empresas que patrocinam as obras. +As reformas no mosteiro e a colocao de um novo calamento no largo de So Bento devem consumir US$ 200 mil. +Esperava aparecer como o nico responsvel pelo plano de estabilizao. +O fracasso das medidas antiinflacionrias poderia lhe custar o acesso ao Planalto. +Desta forma, para se proteger de uma reviravolta inoportuna dos ndices econmicos, FHC e seus amigos do governo tiveram o cuidado de fixar um calendrio vantajoso. +Foram detidas 25 pessoas no escritrio e em outros cinco endereos ligados a Castor de Andrade. +As listas apontam quase todas as divises e departamentos da Polcia Civil do Rio como recebedoras de quantias mensais. +Minha experincia, nos dois ltimos anos, supervisionando programas de qualidade total dentro do grupo Rhne-Poulenc tem mostrado isso com frequncia. +Cabe ao empresrio, ao empreendedor, seja pequeno, mdio ou grande, criar o ambiente adequado para a valorizao de um recurso humano com essas qualificaes. +Do contrrio, todo o esforo feito estar destinado ao fracasso. +Jovens estudantes na Belo Horizonte dos anos 20, o futuro mdico Juscelino Kubitschek e o futuro advogado Jos Maria Alkimin costumavam sair juntos em busca de moas namoradeiras. +Iam aos bailes no Automvel Clube e reparavam nas poucas donzelas que ousavam frequentar os cafs. +Um dia Juscelino comunicou a Alkimin que estava namorando a moa Sara Lemos, filha do poltico Jayme Lemos. +Alkimin achou que tambm era hora de firmar namoro e pediu a JK que o apresentasse s amigas da namorada. +Para ele, os clubes precisam encontrar uma frmula que concilie os dois aspectos. +Segundo Nujud, as contas do Corinthians ainda no foram fechadas, para se saber se o clube teve lucro ou prejuzo. +Qual o maior vexame que j deu? +Sempre os mais recentes so os piores. +Ao parar de fumar, acabei perdendo a estribeira algumas vezes. +Nunca tive muito interesse, apesar de j ter experimentado. +Boa notcia: o Parreira j comprou sua Mitsubishi! +Claro, a nica que lhe d garantia de ir at a Copa de 98! +Porque no escala aquele goleiro jnior, o Pitarelli. +Que defendeu os trs pnaltis do So Paulo. +E os so-paulinos podem se conformar com o Zico. +Que perdeu apenas 4 pnaltis em sua vida. +Rarar! +Pssima Notcia: o Brizola lanou sua biografia em vdeo. +Campanha no ar! +Pelo preo simblico de CR$ 1.000. +Simblico?! +Em se tratando do vdeo, pra mim uma fortuna. +Simblico seria CR$ 1,00. +E olhe l! +A a gente tirava a fita e gravava em cima! +Ento eu vou lanar o Prmio Simblico! +Pago CR$ 1.000 pra quem assistir o vdeo at o fim. +O Roberto Dvila no vale! +Porque foi ele que fez! +Acho que o nico que viu at o fim! +As aparncias so as principais vtimas do sarcasmo de Altman, desde as peruas que discutem a cor de seus cabelos at a noiva, que usa aparelho, escovando os dentes alegre diante do espelho. +O humor de Altman vai na direo das convenes de uma sociedade que, tendo que aceitar tudo sob as aparncias, acaba perpetuando uma vasta rede de hipocrisia. +Pertencemos a um grupo de caridade. +Fazemos um nmero de dana. +Danamos em hospitais e manicmios, diz uma das tias do noivo, uma senhora, para as colegas peruas. +H uma boa dose de realismo tambm. +Os personagens de Cerimnia de Casamento parecem sados das pginas de recm-casados do The New York Times, uma das sees mais provincianas da imprensa mundial e de maior leitura num jornal que vende a imagem de um dos mais cosmopolitas do mundo. +O diretor de crdito agrcola do Banco do Brasil, Said Miguel, disse ontem na Comisso de Agricultura da Cmara dos Deputados que os agricultores j renegociaram R$ 1,7 bilho das dvidas em atraso. +Faltam serem renegociados mais R$ 700 milhes. +Os acordos j feitos abrangem 33 mil agricultores. +O financiamento pblico seria baseado em fundos sociais (FGTS, FAT), na atrao de recursos de penso e nas parcerias com a iniciativa privada. +A taxao de grandes fortunas e o dinheiro de privatizao -- de estatais no-estratgicas (exclui minrios, petrleo e telecomunicaes), complementaria esses fundos. +O ex-presidente Fernando Collor, sua mulher Rosane e o secretrio Lus Carlos Chaves jantaram anteontem noite no restaurante Golden Horn, em Aspen, no Colorado (EUA). +Durante o jantar, pessoas que estavam em uma mesa prxima de Collor e Rosane tiraram fotos. +Embora no fosse o alvo, o ex-presidente assustou-se com os flashes. +Na sua avaliao, a oposio sabe que o plano bom, mas no quer aprov-lo agora, pois seria bater palma para o adversrio. +Votar contra seria perigoso porque o plano tem apoio popular. +Segundo o presidente da Matis-Paris Brasil, Loureno Lopes, 44, a empresa francesa foi escolhida por ter mais estrutura e nmero de produtos para clnicas de beleza, medicina esttica e cirurgia plstica. +Alm dos produtos, a Matis oferece infra-estrutura a seus franqueados. +A empresa possui laboratrio prprio na Frana e fabrica os equipamentos usados em suas clnicas. +Os produtos so profissionais (para institutos) e para o consumidor. +So Paulo foi literalmente invadida pela cozinha japonesa. +E o tempo vem mostrando que o fenmeno no mero modismo. +A cidade tem uma larga populao de origem japonesa e isso sustenta a proliferao e a qualidade de seus restaurantes. +Mas se em muitas casas japonesas da cidade o sashimi e os sushis so os carros-chefes em outros se destacam por iguarias nem to comuns. + o caso do Iti Fuji, restaurante fundado 12 anos atrs pelo japons Hashiro Yawata, 74 (proprietrio de um dos mais antigos restaurantes japoneses da cidade, o Kokeshi, aberto h 33 anos). +Trinta empresrios argentinos e 25 brasileiros se renem na prxima sexta-feira em seminrio da FGV para debater perspectivas do setor privado nos dois pases com o Mercosul. +Os empresrios brasileiros acreditam que os setores mais propcios a investimentos na Argentina so: cerveja, celulose, alimentos, petroqumica, telecomunicaes e gs. +Segundo Luiz Estevo, Collor, vestido com uma cala marinho e camisa de algodo branca, sem mangas, estava confiante no trabalho dos advogados. +Tambm esteve na Casa da Dinda Euncia Guimares, amiga de Rosane. +Collor recebia telefonemas o tempo inteiro, dos advogados, de polticos amigos e de pessoas que prestavam solidariedade. +Um amigo da famlia ligou para informar sobre o estado de sade do irmo do ex-presidente, Pedro Collor, que est sob tratamento nos Estados Unidos. +Subiu ao palco com o candidato a vice, Aloizio Mercadante, e os candidatos ao governo do Estado, Jos Dirceu (PT), e ao Senado, Luiza Erundina (PT) e Joo Hermann (PPS). +Lula respondeu perguntas do pblico sobre sade e cultura. +Mercadante foi escalado para responder quando o tema foi aposentadoria. +Apartamento na rambla Armenia, Montevidu. +Valor: US$ 69 mil. +50% de apartamento na avenida Atlntica, Copacabana, zona sul do Rio. +Valor da frao: US$ 100 mil. +A Reuters Holdings, baseada em Londres, pretende lanar um servio financeiro por TV em meados do ano. +Inicialmente restrito Europa, a empresa o tornaria mais tarde disponvel em todo o mundo. +Entre os teens, os tringulos esto em alta com direito a formao de casais a trs. +No filme, uma garota (Alex) colocada por engano no mesmo quarto de dois rapazes e se apaixona por Eddy. +Mas Eddy ama Stuart e eles acabam se desencontrando. +A Eufrasio Veculos, revendedora Ford, aposta no showroom sazonal e se prepara para comear a atuar em Campos do Jordo por dois meses. +Pesquisa Update, da Cmara Americana, revela que os empresrios esperam queda na inflao com o real. +O ndice mensal de expectativa de inflao teve este ms o resultado mais baixo desde que foi criado h trs anos. + verdade. +Sei que agora tenho um longo caminho a percorrer. +Tenho que estudar muito. +Este foi meu primeiro filme, mas sei que posso ser uma grande atriz. +Sinto isso. +Quero ter mais experincia, estudar e me dedicar 100% a isto. +Quero ser uma das melhores atrizes do mundo. +Este meu objetivo. +E como voc se sente como estrela de cinema? +Meu Deus! +Tenho muito trabalho a fazer antes de me sentir assim. +Mas as coisas que tm acontecido com mim so espantosas. +Eu venho de um bairro muito pobre, muito pobre de Nova York. +Voc no pode imaginar que quo longe eu venho. +Eu me tirei das ruas e comecei a estudar. +Aprendi a falar direito e tentei fazer minha vida melhor para mim. +Quando se est num ambiente como o que eu vivia, voc no sabe que existe todo um mundo diferente fora dali. +Tive muita sorte mesmo. +Eu podia ser uma drogada hoje, podia viver na rua. +Espero que possa voltar l e dizer s crianas que elas no precisam ficar presas l, que existe todo um outro mundo fora dali. +Hoje as coisas esto melhores para a comunidade latina nos EUA. +At em Hollywood, h pessoas como Sonia Braga e Andy Garcia. +Quando eu era criana, tinha vergonha de ser latina. +Alec Eu quero viver, eu quero no fazer nada. +Ali, no torneio que encerrava a temporada 93, Steffi aguardava mais um trofu, olhando para o cho, sem ligar para as 12 mil pessoas em volta da quadra. +Minutos antes, tinha pulverizado a espanhola Arantxa Sanchez. +Nos discursos de agradecimento em Wimbledon e Us Open, meses antes, a alem lembrara o pblico de que no ficava feliz em vencer torneios na ausncia de Monica Seles, afastada das quadras depois de ser esfaqueada na quadra. +A proposta da Anfavea uma indexao disfarada. +Do assessor especial da Fazenda para preos, Jos Milton Dallari. +Chegaram famintos e esfarrapados s cabeceiras do Purus aps quatro meses de viagem. +Mas desvendaram o mistrio da sua ligao com outros rios, feita atravs de canais abertos pelo homem para o comrcio e o contrabando. +De volta ao Rio de Janeiro, Euclides preparou alguns dos mapas que serviram de base para o tratado de fronteiras entre o Brasil e o Peru. +Foi assassinado em agosto de 1909, um ms antes do acordo entre os dois pases. +Escrevia poca Um Paraso Perdido, livro sobre a Amaznia, interrompido com a morte. +A tarefa foi transferida a Falco. +Trata-se de um candidato que estreou em comcio sob vaias e quer, pela via do preconceito, enfraquecer quem lidera as pesquisas, disse Falco sobre FHC. +Ns no vamos dar corda ao debate rasteiro, acrescentou o presidente do PT, que acha improvvel o STF permitir a candidatura de Jos Sarney por um partido que no seja o PMDB. +Os motoristas da EMTU (Empresa Metropolitana de Transportes Urbanos), empresa do governo do Estado, tm o mesmo piso salarial das empresas privadas. +Segundo a EMTU, cerca de 215 mil pessoas utilizam diariamente o corredor do trlebus. +A famlia do bicheiro Jos Scafura, o Pirunha, espera para qualquer momento a libertao de Andr, 15, sequestrado na noite de tera-feira. +Neto de Pirunha, Andr foi capturado perto de sua casa, na Abolio (zona norte). +A famlia diz estar reunindo os US$ 100 mil do resgate. +Segundo o pai de Andr, Luis Carlos Scafura, os sequestradores provaram que ele est vivo, ao responder corretamente algumas perguntas sobre a vida do rapaz. +Scafura afirmou que os sequestradores j ligaram trs vezes. +Na sequncia, em 1498, Vasco da Gama chegou s ndias e inaugurou uma nova era. +Um cabo, ensinam os dicionrios, uma faixa de terra que entra pelo mar o oposto de fiorde, que uma poro de mar que avana pelo continente, como acontece na Noruega e Sucia. + o mesmo que capo, cabedelo ou promontrio, extenso de terra maior que a ponta e menor que a pennsula. +As vendas brutas caram de US$ 3,78 bilhes para US$ 3,57 bilhes, consequncia de redues tanto no mercado interno como no externo. +A receita lquida, excluda a carga tributria, foi de US$ 1,15 bilho em 93, contra US$ 1,27 bilho em 92. +O mercado brasileiro de cigarros apresentou uma queda de 6,5% com o total de unidades vendidas passando de 127,8 bilhes em 92 para 119,5 bilhes no ano passado. +A Souza Cruz, que lidera o mercado, teve sua participao reduzida de 83% em 92 para 79,6% em 93, consequncia de uma retrao de 10,5% nas suas vendas. +Durante a apresentao do balano, Mton Cabral disse que a empresa reagiu no final do ano, voltando a deter mais de 80% do mercado, graas ao sucesso da marca Derby. +A Ensec, brasileira, foi uma das cinco empresas pr-qualificadas para participar de concorrncia de US$ 11 milhes para reconstruir o sistema de segurana do estacionamento do World Trade Center, em Nova York, destrudo com a exploso de um carro-bomba. +O Sinicesp impugnou concorrncia da Prefeitura de Santana de Parnaba argumentando que o edital para obra de infra-estrutura contraria a Lei 8.666. +O hotel ter cinco alas com motivos de cada um dos cinco esportes mais populares dos Estados Unidos futebol americano, basquete, beisebol, tnis e surfe. +A segunda fase do projeto prev a construo (para 1995) do All-Star Music Resort, no mesmo estilo e com mesma capacidade, que homenagear os estilos musicais do country, jazz, rock, calypso e musicais da Broadway. +Entre os catlicos que se intitulam no-praticantes, 94% no tomam deciso baseado nos conselhos religiosos. + mais alto do que os que se dizem ateus ou sem religio (93%). +Indagados sobre se concordavam com a posio da Igreja com relao ao uso de camisinha, 74% discordaram totalmente e 13% discordaram em parte. +Convencido de que Munhoz tem uma boa retrica, o publicitrio pretende deixar o candidato falar o que quiser para, antes da gravao final, decidir o que ir ao ar. +Centrado no slogan Fome e desemprego . Agricultura a soluo, Munhoz vai falar durante o tempo a que tem direito sobre a necessidade de aprimoramento do setor. +A Folha realizou um teste em um dos nibus que integram a nova linha Penha-Lapa, acompanhando a dcima viagem de ontem. +A principal diferena verificada a sensao de frio na barriga quando se passa pelas curvas do elevado Costa e Silva (Minhoco), mesmo em uma velocidade mdia de 60 km/h. +Quem ainda no escolheu sua carreira ou sonha dar uma guinada na vida profissional pode recorrer aos servios de orientao vocacional oferecidos por algumas instituies em So Paulo. +Geralmente procurados por estudantes adolescentes, esses servios so abertos a qualquer interessado e tm a vantagem de ser gratuitos ou cobrar preos baixos se comparados aos de empresas de consultoria. +O objetivo do projeto, segundo Miro, facilitar a investigao policial e a obteno de provas contra o crime organizado (delitos praticados por atividades de quadrilha ou bando). +Uma das novidades a permisso para a polcia se infiltrar em quadrilhas para obter provas. +Atualmente, esse tipo de prova no aceito na Justia. +O projeto tambm permite polcia impedir, interromper e interceptar a escuta e a gravao das conversas telefnicas. +Mas isso s teria validade com a aprovao de um projeto de lei complementar Constituio, regulamentando a escuta telefnica. +Esse projeto tambm do deputado do PDT e j foi aprovado pela Cmara. +Quem queria comprar, por exemplo, meia dzia de pes e um litro de leite, mas s tinha cruzeiros reais no bolso, teve que enfrentar fila de at duas horas. +Um consumidor que tivesse apenas uma nota de CR$ 50.000,00 para fazer uma compra de valor menor tinha de solicitar um cheque de converso no valor aproximado da mercadoria e receber o troco em cruzeiros reais. +As vitrinas sempre guardam surpresas, variando desde o caro refinamento da avenida Montaigne (que concentra a alta costura, no Champs Elyses) at as pechinchas nas pequenas lojas espalhadas pela cidade e nos magazines. +Paris ... o encanto dos sentidos. +Mesmo abstraindo-se tudo o que a cidade representa como patrimnio cultural da humanidade, ainda assim ela resulta parisdisaca, adornada de todo o sortilgio. +Com essas definies, a equipe teria condies de estabelecer as regras o real. +Amaral disse que as reservas internacionais sero mesmo a garantia da nova moeda. +Existem algumas opes de como fazer isso, disse Srgio Amaral, chefe de gabinete do ministro Rubens Ricupero. +O real ter que ter uma referncia em relao ao dlar, segundo Amaral, que no quis dizer se o cmbio ser fixo ou varivel. +Primeiramente, gostaria de dizer que o vosso caderno D+!!! +Adoraria se vocs engordassem um pouco mais o Folhateen. +E eu gostaria de saber como fazer para adquirir a carteirinha de estudante. +No interior, o jeito mais fcil procurar o grmio estudantil ou a direo de sua escola. +Eles devem ter informaes para orientar voc. +O ex-deputado Ibsen Pinheiro (PMDB-RS) tambm no quis falar sobre a absolvio do ex-presidente. +Ex-presidente da Cmara, Ibsen conduziu a votao autorizando o impeachment de Collor, em 29 de setembro de 1992. +O prefeito de So Paulo, Paulo Maluf (PPR), e o candidato derrotado do PMDB Presidncia, Orestes Qurcia, tambm no quiseram comentar o resultado. +O meia Mazinho deve substituir o meia defensivo Mauro Silva segunda-feira contra os Estados Unidos pela segunda fase (oitavas-de-final) da Copa do Mundo. +O tcnico Carlos Alberto Parreira afirmou ontem que a seleo brasileira precisar tocar mais a bola contra um adversrio que ele prev muito defensivo. +O nvel de detalhamento do atlas Folha/The New York Times no tem similar no mercado brasileiro. +Os mapas de pases, como os do terceiro fascculo, mostram 16 tipos de vias de comunicao (de estradas a aeroportos de trfego local e internacional), rios, lagos, cachoeiras, montanhas (acompanhas da altitude), parques nacionais, reservas de gs natural etc. +Senti que eu j conhecia o Brasil antes de ter vindo aqui. +Saar descobriu no Brasil o bambu verde. +Sua idia fazer na 22 Bienal uma instalao com bambus no segundo andar que seja vista por cima, do terceiro, com toda sua cor e padro. +A partir de hoje a rea de convivncia do Sesc Pompia abriga instalao indita do artista plstico Nuno Ramos, Montes. +O trabalho faz parte do projeto Exerccios Visuais do Sesc Pompia, que visa a ocupao inusual de espaos da ex-fbrica. +Os integrantes dos movimentos Pinheiros Vivo e Vila Olmpia Viva receberam com alegria a notcia. +Acho que o Consema foi sensvel ao que est acontecendo. +A obra vai causar um impacto to grande na regio que deve ser analisada pelo Estado, disse Horcio Galvanese, do Pinheiros Vivo. +Para o arquiteto Siegbert Zanettini, do Vila Olmpia Viva, agora um rgo independente est analisando o impacto da obra sobre o meio ambiente. +Segundo Zanettini, a anlise do Consema foi resultado do compromisso assumido, na sexta-feira passada, pelo governador Fleury. +O ndice de Preos ao Consumidor (IPCA) de Belo Horizonte fechou a ltima quadrissemana de novembro com variao de 3,12%, contra os 2,54% registrados em igual perodo de outubro. +Em comparao quadrissemana anterior, quando o IPCA ficou em 3,28%, houve reduo. +O dado do Ipead (Instituto de Pesquisas Econmicas e Administrativas/MG) e mede os gastos das famlias com renda de 1 a 40 salrios mnimos em Belo Horizonte. +O IPCR, para famlias com renda de 1 a 8 mnimos, ficou em 3,06%, contra os 2,85% do mesmo perodo de outubro. +Teremos, ento, um meio-campo ainda mais slido na proteo zaga e mais inteligente, veloz e agressivo, na combinao com o ataque. +Teremos o balano entre defesa e ataque que tanto Zagalo e Parreira defendem, mas que o time no revela em campo. +Alberto Helena Jr., 52, colunista da Folha. +Sua coluna na Copa diria. +Fria Metal entrevista os integrantes do Ramones, banda surgida em Nova York na dcada de 70. +Johnny (guitarra), Joey (vocal), CJ (baixo) e Mark (bateria) falam sobre o surgimento do grupo, a influncia de Iggy Pop, processo de criao e o revival do punk considerado ridculo e ilegtimo por CJ. +Entre os clips que o programa exibe esto Blitzkrieg Bop, I Wanna Live e Poison Heart. +Furia Metal MTV, 21h30. +Entendo que a minha misso no torcer. + o dilaceramento permanente desta profisso possessiva, exclusivista, desconfiada e ciumenta. +Isto no impede que, no ntimo, eu deseje alguma espcie de uma complexa, delicada e difcil soberania para ns. +Na hora da invaso da delegacia, s havia um motorista no planto. +Os invasores quebraram portas, janelas, grades e cadeados. +Os outros 13 presos no tentaram fugir. +O delegado disse que tm se tornado frequentes os linchamentos na regio. +Citou outros ocorridos recentemente em Carinhanha, Santa Maria da Vitria, Paratinga e Guanambi. +Com esse caso em Caetit, chega a 14 o nmero de linchamentos registrados na Bahia desde o incio deste ano. +O aquecimento das vendas nos ltimos dois meses fez com que o empresrio carioca Arthur Sendas desengavetasse antigo projeto de construo de shopping center, hipermercado e conjunto residencial em rea do grupo localizada na Via Dutra, no final da Linha Vermelha. +O grupo Sendas entra com o terreno de 154 mil metros quadrados, e o Nacional Iguatemi Empreendimentos, dono de shoppings centers no Nordeste e scio de Tasso Jereissati no Iguatemi de So Paulo, com os recursos para bancar o empreendimento. +No sbado, Goldman relatou a conversa a alguns quercistas no Palcio Bandeirantes. +Ele disse que a conversa foi preliminar. +Goldman defendeu a necessidade de novas negociaes. +Em sua avaliao, a conversa com o deputado fluminense mostrou que ambos desejam a aproximao. +O msico e humorista Juca Chaves inaugura no dia 5 de outubro, a partir das 22h, um novo teatro em So Paulo. +O Jucabar-Theatro Inteligente vai funcionar na rua Major Sertrio, 661, no centro de So Paulo. +A renda da noite de inaugurao ser doada ao Fundo Social de Solidariedade do Estado. +33... dias sero completados na prxima segunda-feira, dia de eleio, sem que os vereadores da Cmara de So Paulo votem algum projeto importante. +Dos 55 vereadores, 24 so candidatos Assemblia Legislativa, Cmara dos Deputados ou Senado. +Se todos conseguirem ser eleitos, quase metade (43%) dos vereadores paulistanos vo trocar de Casa Legislativa. +Yoshiaki Nakano -- economista, ex-secretrio especial de assuntos econmicos do Ministrio da Fazenda e ex-secretrio nacional de poltica econmica (gesto Bresser Pereira). +Pasta indefinida. +Vladimir Rioli -- Pode ficar com a presidncia do Banespa. + economista e ex-vice-presidente do Banespa (governo Fleury). +Como num grandioso mea culpa, os ltimos filmes que Eastwood dirigiu Os Imperdoveis e Um Mundo Perfeito apontam justamente para uma crise do heri, de sua autoridade restauradora, de sua integridade monoltica. +Em consequncia, pe a nu uma crise dos prprios gneros com que trabalha. +Em Um Mundo Perfeito, o tema e seu desenvolvimento so de uma clareza exemplar. +No Texas, em 1963, um menino sem pai, Philip Perry (T.J. Lowther), tomado como refm por um criminoso fugitivo, Butch Haynes (Kevin Costner). +Ambos partem numa fuga para o norte (rumo ao pai de Haynes, que mora no Alasca). +O programa sobre a 2 Guerra Mundial traz informaes sobre 2.100 eventos, tem 900 fotos e 140 mapas de batalhas, permitindo estudar as tticas usadas na guerra, alm de textos e vdeos de gravaes originais. +Quem prefere as misses espaciais tem disposio cerca de oito horas de informaes sobre 1.600 misses, incluindo o programa Apollo e o passeio do astronauta Neil Armstrong na Lua. +A organizao tambm iniciou um inqurito para averiguar o massacre de cerca de 500 mil pessoas. +Encarregado do caso, o marfinense Rene Degni-Segui se disse favorvel a um tribunal internacional para julgar os acusados. +Pessoalmente prefiro um tribunal internacional a dependermos dos tribunais nacionais (de Ruanda), que tero dificuldade em ser imparciais, disse o professor de direito da Costa do Marfim. +A surpresa descrita por Pedro Collor atingiu todos os partidos que integram a chamada Nova Fora da poltica local, liderada por Lessa. +At o final da tarde de ontem, nenhum dos partidos da Nova Fora havia se manifestado sobre o assunto. +Lessa disse que est disposto a ouvir uma proposta formal do governador. +O Congresso Internacional de Direitos Autorais enviou, na ltima sexta, uma recomendao ao ministro da Cultura, Luiz Roberto Nascimento Silva, para reativar o rgo governamental que cuidava da rea da propriedade intelectual. +O CNDA (Conselho Nacional de Direito Autoral), que respondia pela fiscalizao, consulta e assistncia nas matrias relativas a direitos autorais, foi desativado pelo governo Collor. +FHC e Malan no esclareceram, at o momento, quanto o governo gastou e quando foram comprados os papis. +Ainda assim, s o anncio de que as garantias sero adquiridas junto ao mercado poder elevar os gastos com as futuras compras, raciocinam senadores como Espiridio Amin (PPR-SC), Ronan Tito (PMDB-MG) e Gilberto Miranda (PMDB-AM). +Assessores da equipe econmica j tm pronta parte da argumentao. +FHC e Malan devero defender que o uso das reservas foi necessrio para salvar o acordo da dvida, sem o qual o pas no normalizar suas relaes financeiras internacionais. +Para explicar as compras em sigilo, o argumento justamente a necessidade de evitar o encarecimento dos papis. +O acordo da dvida implica a troca dos papis atuais por uma combinao de cinco tipos diferentes de novos papis. +A operao dar um desconto inicial de US$ 4,3 bilhes sobre o valor do dbito. +Essas condies, para renegociar US$ 35 bilhes, foram aprovadas pelo Senado no final de 93. +O mesmo no ocorre com a mistura de tinta para a parede. +Segundo a consultora de cores Renata Loureno, o trabalho pode ser feito por qualquer pessoa seguindo as instrues da lata, sem a necessidade de um pintor profissional. +Mas deve-se tomar o cuidado de acabar com infiltraes na parede para que a tinta possa ser bem aproveitada e dure mais tempo. +lvaro Vidigal, da Bovespa, registra a prudncia dos candidatos s eleies de hoje, que esto sabendo preservar a instituio acima dos interesses pessoais. +Preocupao de Vidigal: Manter junto s autoridades a imagem de seriedade cuidadosamente cultivada no decorrer desses anos. +A Secretaria Estadual da Sade investiu R$ 11 milhes na campanha, de acordo com o diretor do Centro de Vigilncia Epidemiolgica, Wagner Costa. +Na primeira fase, no dia 11 de junho, foram vacinadas cerca de 3,2 milhes de crianas em So Paulo. +Neukirchen disse que a proposta apresentada aos bancos inclui uma injeo de 2,7 bilhes de marcos (US$ 1,55 bilhes) para aumento do capital acionrio. +Alm disso, a companhia pede que os bancos aumentem suas linhas de crdito em 500 milhes de marcos (US$ 287,1 milhes) e concedam uma moratria de trs meses no pagamento das dvidas. +No mercado, estima-se que as dvidas do grupo superam os nove bilhes de marcos. +Os bancos devem responder proposta do grupo at o dia 12. +A crise no MG aprofundou-se no incio de dezembro com o declnio nos preos internacionais do petrleo. +A pesquisa, divulgada no ltimo sbado, aponta o candidato do PSDB em primeiro lugar, com 48% das intenes de voto. +A definio do tempo no horrio eleitoral gratuito foi encaminhada pela corregedoria a todos os partidos que disputam a sucesso do governador Luiz Antonio Fleury Filho. +A escolha dos tcnicos Johan Cruyff, do Barcelona da Espanha, e Tel Santana, bicampeo mundial pelo So Paulo, como colunistas da Folha durante a Copa dos EUA recebeu elogios de ex-jogadores da seleo brasileira. +Todos lembraram a importncia dos dois treinadores na histria do futebol e no cenrio atual mundial. +Folha -- Como vai atuar no jogo contra o Palmeiras? +Sandoval -- Vou entrar com determinao, sabendo que vai ser difcil. +Confio na nossa equipe e espero sair do Parque Antarctica com a vitria. +Uma nova escalada obscurantista na China afastou ontem de Cannes o cineasta Zhang Yimou, diretor do clebre Lanternas Vermelhas. +Os negcios com ouro na BM&F (Bolsa de Mercadorias e Futuros) somaram 54 toneladas em maio, o que representa uma queda de 2,70% em comparao ao ms imediatamente anterior, quando foram movimentadas 55,5 toneladas, de acordo com informaes da entidade. +A mdia diria de negcios com o metal em maio foi de 2,43 toneladas. +Em relao ao mesmo ms do ano passado, quando os negcios atingiram 139,8 toneladas de ouro, a reduo de 61,37%. +A mdia diria naquele ms foi de 6,6 toneladas, segundo dados da Bolsa de Mercadorias & Futuros. +Se no fizssemos isso, os jogadores sofreriam mais. +Os horrios das refeies foram adaptados ao fuso local, disse Sant'Anna. +Apenas para estadas mais breves, de um a dois dias, o preparador teria optado por manter o fuso de Los Gatos. +O Conia, por exemplo, a parte do congresso que discute o uso da informtica pelos profissionais liberais. +O Conia ter 18 palestras dirigidas s reas de medicina, odontologia, veterinria, arquitetura, urbanismo, engenharia e direito. + nico no mundo. +Felicito. + uma coisa a se estudar. +Mas deixa escapar o que pensa. +Na cabea do treinador ou no papel, o esquema ttico pode ser ofensivo. +Mas no o ser, na prtica, se no houver determinao para vencer. +J Vail oferece entre outras coisas espetculos ao longo do vero realizados num anfiteatro aberto. +O destaque, sem dvida, o bal Bolshoi da Rssia, que h pelo menos cinco anos consecutivos se apresenta na cidade. +O Flamengo ficou na espera do Grmio, procurando apenas aproveitar o contra-ataque. +Mesmo assim, no criou muitas chances. +O gol da vitria flamenguista foi marcado aos 20min do segundo tempo. +Magno desceu pela direita e cruzou para a rea. +Nlio teve apenas o trabalho de escorar para o gol vazio. +Emissora: Srie em seis episdios, exibidos de segunda a quinta, na Cultura. +Propaganda - O Poder da Imagem o tipo de srie que poderia ser adotada com proveito nos currculos escolares. +Os responsveis pelo programa vasculharam uma multido de arquivos de filmes (russos, norte-americanos, franceses, ingleses, alemes) e selecionaram imagens produzidas pelas agncias oficiais desses pases que compem uma formidvel ilustrao da histria do sculo 20, tal como vista por seus protagonistas. +O governo do Estado diz que a greve ilegal. +Os professores da UFPB (Universidade Federal da Paraba) tambm entraram em greve geral ontem. +Eles reivindicam 105% de reposio de perdas salariais. +Os funcionrios da universidade esto em greve desde o ltimo dia 19. +Patrcia -- Minha literatura vem da imagem, da TV, do cinema. +Eu tento usar o processo de edio, como se trabalha em uma mesa de edio. +Folha -- Como teu mtodo? +O ingls John Ritchings vai escrever um livro sobre os meses que foi torturado pelo galo da vizinha, diz o jornal. +O galo, Corky, cantava das 3h s 7h diariamente e Ritchings no conseguia dormir. +A estrela smbolo do PT vai emoldurar com destaque o cenrio dos programas do candidato Luiz Incio Lula da Silva. +Seus programas de rdio tero reportagens de rua, vetadas na TV. +Segundo Markus Sokol, coordenador de comunicao, a estratgia de comunicao foi delineada em um seminrio realizado em dezembro passado. +Executivos em cargos de liderana assumem seus cargos com mais uma pesada funo: implementar cada passo da estratgia que eles prprios esboaram. +Em junho, Christensen esteve no Brasil a convite da Fundao Dom Cabral de Belo Horizonte, para acompanhar a segunda etapa do curso STC-Executivo. +As construes lembram vagamente uma verdadeira vila de pescadores. +Perto de qualquer uma, o cenrio de Tropicaliente parece uma grande aldeia de milionrios. + bonito isto daqui, dizia admirado um pescador nativo, que participava de uma gravao como figurante no ltimo dia 30. +Licena potica parte, as praias e as dunas de Fortaleza foram escolhidas como cenrio de Tropicaliente por Paulo Ubiratan, diretor-artstico da Globo, por causa da beleza natural do lugar. +Crianas mais velhas que nunca seguiram a dieta podem ter retardo mental, desenvolvimento motor prejudicado e alteraes significativas do comportamento (isolamento e agressividade). +Pesquisas tm mostrado que o fenilcetonrico deve seguir a dieta durante toda a vida. +Os adultos que voltam a comer protenas podem ter piora da concentrao e da memria e uma maior agitao. +Quando astronautas embarcarem numa estao orbital ou, eventualmente, estiverem a caminho de Marte, tero como companhia necessria alguns bilhes ou trilhes de micrbios comiles. +A Nasa (agncia espacial dos EUA) encomendou empresa Micro-Bac o desenvolvimento de um sistema de purificao de esgoto utilizando bactrias conhecidas por devorarem material orgnico. +O estudo custou US$ 400 mil. +Os testes de laboratrio deram certo, e agora a empresa se prepara para comercializar essa mini-usina tratadora de esgotos. +Em um dos subprodutos mais irnicos da pesquisa, a tecnologia criada para uma casa orbital de bilhes de dlares, a estao Freedom, poder facilitar a construo de casas populares na Terra, segundo Ercole Amrico Carpentieri Jr., diretor da filial brasileira da Micro-bac. +A tecnologia pode reduzir a necessidade de rede de esgotos em determinados locais. +O que deve ser destacado de forma resumida em um item sobre qualificaes profissionais so os pontos fortes da experincia e da formao acadmica e os atributos que podem diferenciar o candidato no mercado. +Segundo Laerte Leite Cordeiro, consultor especializado em recolocao, o currculo deve ser dividido em trs partes: rea geral de interesse (administrativa, contbil, financeira etc.), qualificaes profissionais e descrio da trajetria de empregos (empresas, cargos, perodos). +Comenta o contrabando de plutnio, dizendo que sria advertncia para que governos controlem os excessos deste material que o fim da Guerra Fria criou. +O jornal kuaitiano diz que o governo do pas fechou um salo de beleza por uma semana por infringir a lei que probe mulheres de trabalhar em sales para homens e vice-versa. +O Houston Rockets conquistou sua 27. vitria na temporada da NBA (contra apenas quatro derrotas), em casa, sbado, ao vencer o Philadelphia 76ers por 100 a 93. +Mais uma vez o piv Hakeem Olajuwon foi o fator de desequilbrio a favor do Houston, um dos mais destacados times do campeonato. +Ele converteu 23 pontos e ganhou 17 rebotes. +O Philadelphia tem apenas 12 vitrias e j perdeu 19 vezes. +Contra o Houston, o cestinha da equipe foi Jeff Honaceck, com 20 pontos, seguido por Clarence Whiterspoon, com 16. +Lamentavelmente, mais uma vez, um pas do subcontinente sul-americano suspende as garantias do Estado de Direito. +Desta feita, porm, no foi uma aventura castrista ou uma injuno poltica qualquer que levou o governo venezuelano a este ato extremo, mas sim a conjuntura econmica. +E, de fato, os ventos no sopram em favor dos venezuelanos. +Eleito em 1988, o ex-presidente Carlos Andrs Prez (social-democrata) encetou uma poltica econmica neoliberal que imps sacrifcios populao dando incio a uma srie de violentos protestos. +Prez enfrentou duas tentativas de golpe militar e, a exemplo de Fernando Collor, acabou sofrendo impeachment por corrupo e, hoje, encontra-se numa cela espera de julgamento. +Aprofundando as dvidas, Leila se deixou cair na tentao do duplo. +Se no h mais bossa nova realmente nova, o melhor mimetizar o criador do estilo, o cantor Joo Gilberto. +O resultado dessa tentativa de metempsicose est no recm-lanado CD Isso Bossa Nova (EMI-Odeon), stimo disco em 12 Anos de sua carreira. +A lei tambm prev que at o final de junho o governo enviar ao Congresso um projeto sobre a elevao do valor real do salrio mnimo. +Se o mnimo sofrer alterao, a Constituio determina que as aposentadorias tambm sejam revistas. +O Mogi Mirim venceu ontem o Unio So Joo de Araras por 2 a 0 em Mogi e deu mais um passo para conquistar uma vaga na Copa Bandeirantes. +Esse torneio reunir os seis primeiros colocados da Srie A-1 e o campeo da A-2 do Campeonato Paulista. +Em So Paulo, no necessrio deslocar-se muito procura de artigos de segunda mo. +H pontos de concentrao de lojas que facilitam a pesquisa de preos. +Entre uma loja e outra podem ocorrer grandes diferenas. +Nas aulas, Candelori usa reportagens de jornais para introduzir seus temas e colocar toda a classe para falar. +Na lista dos assuntos esto a campanha contra a fome, racismo, imigrao nordestina, aborto, corrupo poltica e aborto. +Tem gente que acha a aula intil. +So as mesmas pessoas que acham que sua responsabilidade social se limita a pagar impostos, diz Letcia Zioni Gomes, 16. + preciso cobrar o governo para as coisas funcionarem, completa Bruno Fanizzi, 15. +Fanizzi conta que antes das discusses em aula era uma pessoa de extrema direita, que no pensava nas suas opinies. +A aula boa porque o professor jamais impe a opinio dele. +Ele levanta a discusso e deixa o conflito de idias rolar, diz Manoela Nicoletti, 15. +Segundo Candelori, seu curso de sensibilizao para temas ticos. +Eles no suportariam a aridez da teoria, mas discuto temas como espao pblico e privado atravs de fatos como o comportamento do Itamar no Carnaval, diz. +Sobre o controle antidoping, a Fifa estabelecer uma fax hotline, em operao 24 horas por dia, para que os mdicos das delegaes consultem a organizao da Copa antes de fornecer qualquer medicamento a seus jogadores. +As consultas sero feitas por escrito para evitar contestao. +O brasileiro Joo Havelange consolidou ontem, em Nova York, a sua candidatura para o sexto mandato consecutivo como presidente da Fifa. +O presidente da Uefa, Lennart Johansson, negou que a entidade europia v apresentar oposio a Havelange. +O fonoaudilogo tem uma rea vasta de atuao, apesar de estar muito concorrida. +A gente pode trabalhar com atores de teatro, crianas, na preveno de doenas ou no exame de trabalhadores expostos a rudos. +O atacante do Barcelona obteve o Onze de Ouro, distino outorgada por leitores da revista francesa Onze Mundial. +Recebeu 13.576 votos (33,94%) em 40 mil respostas. +O time anunciou ontem a contratao do craque argentino. +Naquele ano, Fleury j aparecia com 20% das intenes de voto em setembro, um ms antes do primeiro turno. +A menos de uma semana da eleio deste ano, Munhoz no passou dos 12%. +Fleury foi para o segundo turno e venceu o atual prefeito de So Paulo, Paulo Maluf (PPR). +Na pesquisa Datafolha divulgada no ltimo sbado, o primeiro colocado, Mrio Covas, aparece com 47%, nove pontos percentuais acima da soma dos ndices dos demais candidatos. +ACM disse que no conseguiu entender nada do novo plano econmico elaborado pela equipe do ministro Fernando Henrique Cardoso. +Eu estou igualzinho ao presidente Itamar, igualzinho ao ministrio, igualzinho a 150 milhes de brasileiros, que no sabem coisa alguma sobre este plano. +Segundo ACM, quem disser que conhece o plano, de um modo geral, est mentindo. +O governador acrescentou que pedir ao Nosso Senhor do Bonfim para que o plano d certo. +O acordo inclui votar, sem novas emendas, o projeto de Oramento para 95. +Os lderes acertaram que, havendo quorum, o Oramento de 95 s no ser votado hoje se no ficar pronta a publicao do parecer aprovado na Comisso Mista de Oramento, acolhendo cerca de R$ 2,5 bilhes em emendas. +O STJ decide que Halpern e Decaro, indiciados pelo MPF sob a acusao de fraude processual, devem ser julgados pela Justia Federal em So Paulo. +Os deputados petistas Luiz Gushiken (federal) e Luiz Azevedo (estadual) pedem que o Ministrio Pblico estadual reabra processo sobre as importaes. +O partido atua no movimento sindical atravs da Corrente Sindical Classista, filiada CUT. +Controlam ou so influentes em sindicatos importantes em So Paulo, como o dos metrovirios, o da Sabesp e bancrios. +Os sindicatos dos metalrgicos de Betim (MG), de Ribeiro Preto (SP) e da Bahia tambm so da tendncia. +Com o resultado, o time perdeu a chance de garantir antecipadamente a vaga nas quartas-de-final. +A equipe entra na quadra amanh s 20h30 (15h30 de Braslia) para disputar com o Canad, no chamado grupo da morte, um lugar na prxima fase do Mundial (leia matria nesta pgina). +Entre os exemplares destes vinhos que chegaram s nossas praias, todos safra 92, o Bourgueil Lchellerie, milita decididamente na primeira fila. +Ele um vinho ligeiro, de aroma simples de frutas vermelhas e boa acidez, ideal para ser bebido refrescado. +O Chinon Les Granges e o Saumur-Champigny Domaine de la Martinire so pouco mais encorpados. +O Les Granges tem aroma e sabor marcado por morangos e evolui bem na boca, enquanto o Saumur, tambm frutado, mais srio e (levemente) tnico. +O sushiman Oscar Tadaioshi Izumi baixa com hashi e raiz forte hoje nos domnios de Wilma Kovesi para entregar os segredos da cozinha japonesa. +Ningum ignora os terrveis problemas que o pas atravessa, dos quais a corrupo policial e os nveis de violncia so apenas uma faceta. +Ainda assim, de se supor que existam bons agentes de polcia. + particularmente a estes bem como a toda a sociedade que interessa a apurao completa e rigorosa dos massacres. +Toda vez que um crime cometido por representantes do poder pblico fica sem punio, o nus recai sobre a corporao como um todo, gerando um turbilho que indiferncia os bons dos maus policiais. + evidente que quem perde so os primeiros. +O pior, porm, que nesse processo o prprio Estado cai em descrdito e acaba cedendo mais espao s foras do caos. +Folha -- O Mercosul foi objeto de declaraes meio eufricas nos ltimos dias. +Que pontos, no entanto, o deixaram frustrados? +Amorim -- No posso dizer que algo me frustre, levando em conta que h trs anos a idia do Mercosul surgia no Tratado de Assuno. +Talvez preferssemos que o comrcio estivesse totalmente liberalizado, sem as listas de produto em regime de adequao (sobre os quais so mantidos os impostos alfandegrios) que afetam de 5 % a 8 % do comrcio regional. +E como ningum de ferro e Orlando fica perto, duas horas e meia de avio, a turma depois d uma passadinha para um milk shake com Mickey e Pateta. +Aberto desde 1948 no Beverly Boulevard quando os clientes eram Errol Flynn, Frank Sinatra e Billy Wilder, o restaurante Dominick's continua na moita. +Tudo leva a crer que estamos num processo de endemia, sustenta o patologista Bruce Mendes Campos, responsvel pela anlise do sangue. +De 205 pacientes com suspeitas da doena, 47 revelaram-se positivas neste ano. +Estamos assustados, afirmou o diretor do hospital, Aloysio Campos da Paz, professor visitante das universidades de Oxford (Inglaterra), Stanford, Harvard e Nova York (EUA). +Para viabilizar a viagem de pelo menos parte dos 20 integrantes do coral advogados, bancrios, secretrias, estudantes, aposentados e apenas um padre, a diretora diz que se uma empresa se dispuser a cobrir os custos da viagem, o coral oferece em troca a gravao de um disco promocional. +Qualquer doao seria muito bem recebida, diz madre Maria. +Os interessados em viabilizar a participao do coral no festival belga podem entrar em contato com a direo do coral nos telefones (011) 231-5346 e 288-3514. +Olhando com ateno os jogos da Copa possvel chegar a uma concluso oposta de Parreira. +Foi justamente graas arte de alguns de seus jogadores que equipes como a Bulgria, a Romnia e a Nigria chegaram muito mais longe do que jamais tinham conseguido. +Num campeonato em que predominaram as defesas fechadas, o que fez a diferena foi a imaginao ouso dizer, para horror de Parreira, a magia de um punhado de craques: Romrio, Baggio, Stoichkov, Hagi, Brolin... +No final feita uma auditoria para ver se a auto-avaliao feita pela fbrica corresponde realidade. +Segundo Migues, algumas fbricas da Autolatina ainda no atingiram o padro de excelncia Q1. +As fbricas que atingiram receberam uma placa e, em alguns casos, um veculo sorteado para seus funcionrios. +A disciplina de cirurgia plstica da Faculdade de Medicina da USP est lanando um programa de cirurgia plstica em crianas com a colaborao do Banco Real. +O objetivo divulgar as tcnicas mais modernas de tratamento das deformidades congnitas nas diversas regies do pas. +A primeira etapa do programa comea hoje e vai at o dia 2 em Joo Pessoa (PB). +Um novo protocolo desenvolvido por um grupo de mdicos de quatro instituies pblicas de So Paulo est trazendo para o Brasil o Interferon Beta, para o tratamento de pacientes com esclerose mltipla (doena degenerativa que causa enrijecimento muscular, debilidade das funes motoras e perda parcial da viso). +A droga foi aprovada em julho passado pela FDA rgo de fiscalizao de medicamentos dos Estados Unidos para o tratamento da doena. +Informaes pelo tel. (011) 887-8740. +O ex-ministro e candidato presidencial Fernando Henrique Cardoso disse Folha que frequentava no passado os sebos do centro, perto da Faculdade de Direito da USP. +Quando vai aos EUA, ele no se limita a comprar blazers caros. +Tambm vai livraria Barnes and Nobles, em Nova York. +Nem todos tm tempo e pacincia para sebos. +O filsofo Jos Arthur Giannotti diz que se sente angustiado em livrarias e prefere comprar seus livros por catlogos. +Tantos livros, to pouco tempo para ler. +O nibus era da Viao Boa Vista e transportava cerca de 40 passageiros na hora do acidente, segundo a Polcia Rodoviria. +Com a coliso, cinco passageiros ficaram feridos e foram atendidos no Hospital Mrio Gatti. +Todos passam bem. +De um lado, acirrou-se a reao marcadamente militante, mais diretamente identificada com as propostas poltico-ideolgicas das esquerdas organizadas, cujas manifestaes procuravam realar o que se entendia na poca por cultura nacional e popular, rechaando a influncia imperialista e suas armas culturais entre as quais incluiam-se a televiso voltada para o consumo e para a alienao, as formas artsticas americanizadas, a cultura pop e at... a guitarra eltrica. +Esta vertente, francamente conteudista, derivava das experincias realizadas no perodo pr-64 pelos Centros Populares de Cultura (CPCs), ligados Unio Nacional dos Estudantes, que privilegiavam a mensagem e procuravam falar uma idealizada linguagem do povo. +Ele teve importante papel ao manter a poltica externa e muitos assuntos internos fora da esfera decisria da UE, ganhando a simpatia britnica. +J assessores do federalista Delors, que em dez anos na presidncia da CE aumentou os poderes do cargo e entrou em conflito com Margaret Thatcher (antecessora de Major), temem que a escolha de Santer sirva para enfraquecer os poderes do presidente, o que abriria caminho para o eixo Alemanha-Frana consolidar o seu domnio. +O fato de haver razovel consenso acerca desses temas no significa, porm, que seja fcil ou rpido aprovar tais reformas no Congresso. +Elas dependem em boa parte de emendas constitucionais, o que est longe de ser simples de fazer. +Ora, se o presidente chegar posse sem que estejam no mnimo delineados, divulgados e negociados com os setores organizados da sociedade os projetos que consubstanciem tais reformas, torna-se muito mais difcil faz-los aprovar no Congresso no prazo de um semestre definido pelo prprio FHC. +Por tudo isso, no cabe formalismo em excesso. +Cabe, lgico, todo o respeito s prerrogativas do atual governante. +Mas, at por ser tambm o candidato do presidente, FHC deve comear a trabalhar j. +As paulistas Mancha Verde (Palmeiras) e Gavies da Fiel (Corinthians) aceitaram, mas as cariocas se recusaram. +A carta de Stepanenko engrossa desde ontem o inqurito contra o ministro no TSE. +Trata-se de outra evidncia irrefutvel. +Itamar no precisa da ajuda de Vicentinho. +Tem diante de si um portentoso exemplo de desvio de conduta. +Alis, j poderia ter agido. +Seu silncio soa a cumplicidade. +Mas os pontos mais importantes no foram tocados. +No ouvi falar nada sobre arbitragens. +No se fala na cobrana dos rbitros que apitam maldosamente, para prejudicar uma equipe. +Isso aconteceu muitas vezes este ano. +Outro ponto que as bolas que vo ser usadas no campeonato no devem ser escolhidas por interesses comerciais. +Cerca de 300 cubano-americanos protestaram na frente do prdio da misso cubana na ONU, onde as negociaes ocorreram. +Funcionrios do escritrio cubano tentaram abafar os protestos na rua colocando caixas de som nas janelas do prdio com msica alta. +Em Miami, duas bombas caseiras (coquetis Molotov) foram encontradas ontem em frente revista cubana Replica, que apia as negociaes entre Cuba e EUA para tentar resolver a crise. +As bombas no chegaram a ser acionadas. +Se podemos nos orgulhar de nosso futebol, no podemos dizer o mesmo dos nossos rbitros, aqui representados exatamente por Marsiglia. +Falando em futebol, em bom futebol, devemos saudar a chegada de Ronaldo aos Estados Unidos. +As empresas Tejofran e Power informaram ontem, em nota enviada ao TRE (Tribunal Regional Eleitoral), que prestam servios aos comits do PSDB e PMDB. +A Tejofran e a Power so acusadas por Rui Ramos, ex-integrante da campanha tucana, de financiar a estrutura poltica do candidato do PSDB ao governo, Mrio Covas. +Na cheia, entre outubro e abril, quando a mdia mensal de chuvas supera 290 milmetros, a irrigao do arroz feita por inundao. +No perodo mais seco, quando as chuvas no passam de dez milmetros/ms, os produtores plantam soja, que exige menos gua. +Na sua primeira declarao, Queiroz omitiu US$ 1,7 milho em bens. +Entre eles estavam a empresa ticas Trevo, terrenos e pontos comerciais em Salvador. +O presidente do partido no Rio, S Freire, disse que o pedido de cassao foi feito porque Walter Queiroz no teria comparecido s reunies marcadas para que ele explicasse sua nova declarao de renda. +J o sistema de tratamento de esgotos residenciais, com a estao do ABC, dever atingir 150 toneladas/dia. +Somando-se os dois nmeros, chega-se a 500 toneladas/dia, aproximadamente 50% das 1.100 toneladas de poluio orgnica diria jogadas no rio. +Antes da derrota por 2 a 0 na primeira partida entre os dois clubes, quinta-feira no Mineiro, o tcnico botafoguense Renato Trindade desenhou a ttica adversria num quadro negro do vestirio. +Ele circundou com giz os cinco meias. +O segredo do Atltico o quinteto. +Eles vm de trs com a bola e nos dificultam, disse. +Ele foi o responsvel pela incluso de peixes amaznicos no livro dos recordes da International Game Fish Association. +A Polcia Militar do Amazonas est desenvolvendo uma campanha educativa de trnsito. +Ela est alertando os motoristas que saem de Manaus pela rodovia AM-10. +Policiais orientaro os motoristas com placas educativas que indicaro os cuidados bsicos contra acidentes. +Agora suponha que a economia toda vire um bando de gente que ganha mesada e de gente que d mesada. +Para quem recebe em URV, timo. +Para quem paga, pode no ser um grande negcio ter de pagar mais dinheiro todos os dias. + mais ou menos o que est acontecendo na discusso de quem tem imvel com quem paga o aluguel, por exemplo. +Ou do dono da escola com seu pai, que paga a mensalidade. +Se voc tem um pai po duro, no tenha dvidas pea a URV na sua mesada. +Entre os itens importados, devem chamar ateno na Feicon os revestimentos de cermica para piso trazidos da Itlia. +Importados pela Cramus, os pisos de uso comercial (para hotis e shopping centers) destacam-se pela grande resistncia. +Algumas ainda esto sendo calculadas pela Caixa. +Eles tero ainda de pagar multa equivalente a R$ 647,90. +Alm dos dois, tambm foram condenados outros 12 diretores da CEF. +A necessidade de reparar os equipamentos durante a viagem teria alterado os planos de trabalho. +Damascene viaja raramente de navio, mas no um estranho ao mar. +Quando jovem, esteve engajado na Marinha grega. +Conhecia bem grandes embarcaes e saberia como agir numa emergncia. +A situao tende a se agravar, uma vez que nenhuma das partes parece mostrar disposio de recuar. +O governo recebeu ontem outra m notcia: empresrios se declararam contrrios ao CIP. +O motivo que ele obriga a empresa que contratar jovens a manter um tutor para ensinar o trabalho aos novos empregados. +Com isso, dizem, aumentam os custos de contratao dos jovens. +Ao contrrio do que poderia esperar a direita norte-americana na vigncia da Guerra Fria, a vinda de Soljenitsin no pde ser capitalizada em discursos e aparies pblicas. +Por dois motivos: primeiro, a indisponibilidade do escritor, que simplesmente no estava interessado em percorrer os Estados Unidos ou debater suas idias; segundo, seus comentrios sobre a decadncia moral do Ocidente logo o tornaram inconveniente. +Sua ltima apario foi numa formatura em Harvard, em 1978. +Depois disso, caiu no ostracismo. +Sua partida dever ser igualmente discreta. +Seu editor, Claude Durand, disse Folha por telefone, de Paris, que Soljenitsin no tem inteno de fazer qualquer despedida pblica dos Estados Unidos. +Sei que jamais terei uma chance de trabalhar to calmamente de novo. +Na Rssia, serei dilacerado pelos acontecimentos e pelas tragdias das pessoas. +Est visto que nem o conjunto, nem qualquer outra frase dele, d frase negada por Fernando Henrique o sentido que ela tem por si mesma. +A leitura de todo o trecho leva, alis, a outra mentira, esta sobre os tempos de TV do PSDB e do PT. +No a primeira vez, e espero no ser a ltima, que Fernando Henrique e esta coluna se atritam pelo mesmo motivo. +A primeira fez dez anos h pouco. +Foi quando noticiei que Fernando Henrique estava em contatos sigilosos com o governo Figueiredo, oferecendo um plano de conciliao entre o regime que se exauria e o que nasceria. +Para isso, a presidncia no poderia ficar com o candidato bvio na poca, Ulysses Guimares, que supostamente representava o risco de confronto com os militares. +Tancredo tivera com o governo conflitos impeditivos, aqueles que o levaram a dissolver o PP de ento. +No preciso repetir, agora, quem era o homem que Fernando Henrique apresentava como adequado ao plano, sob o argumento de que tinha trnsito nas esquerdas para encabear um pacto conciliador. +O professor Leito de Abreu, que era ministro do Gabinete Civil, no vive para confirmar as visitas ofertantes de Fernando Henrique. +Mas h testemunhas da veracidade da notcia, inclusive o ex-presidente Figueiredo, que repeliu a proposta. +Apesar da veracidade ainda hoje comprovvel, na ocasio Fernando Henrique me fez graves acusaes. +Com a mesma hombridade que exibe mais uma vez. +Pois experimente ir a um show de msica, por exemplo, ofender Tim Maia ou Joo Gilberto na boca do palco, e voc ver que a atitude de Edmundo foi, no mnimo, compreensvel. +Pois , Palmeiras e Corinthians fazem hoje a finalssima do Brasileiro. +Provavelmente ser um belssimo jogo. +O Corinthians ter de atacar os 90 minutos para reverter a enorme vantagem palmeirense. +Como Collor, o novo presidente falou em: reforma do Estado, abertura da economia, avano da privatizao e flexibilizao dos monoplios estatais. +Isto comeou a acontecer no governo anterior ao do presidente Itamar Franco. +Morreu no domingo noite aos 80 anos o cientista Roger Sperry, ganhador do Prmio Nobel de Fisiologia e Medicina de 1981. +O anncio foi feito segunda-feira pelo Instituto da Califrnia para Tecnologia Avanada (Pasadena), onde Sperry trabalhou at 1984. +Na relao divulgada ontem em Washington, o Brasil citado com outros 34 pases. +O Japo o principal alvo da lista por barreiras aos produtos, servios e investimentos norte-americanos, que segundo o USTR, colaboraram para que o dficit dos EUA no comrcio com os japoneses atingisse US$ 60 bilhes no ano passado. +O relatrio dedica 42 pginas ao Japo, cujas barreiras importao de produtos e servios so muito maiores do que as dos demais membros do Grupo dos Sete (pases mais industrializados) e representam um entrave inaceitvel ao sistema de comrcio global. +Tendo em mente o Japo, os EUA reinstalaram recentemente a clusula conhecida como Super-301, mecanismo legal que amplia os poderes de retaliao comercial da Casa Branca. +A Super-301, que j foi aplicada contra o Brasil quando esteve em vigor em 1989 e 1990, estabelece um prazo de 18 meses para que as negociaes bilaterais levem eliminao do que os EUA consideram barreiras ao comrcio. +Depois disto, os EUA podem impor tarifas punitivas de at 100% s importaes do pas ofensor. +F do tcnico so-paulino Tel Santana ( muito modesto), Bianchi v o time paulista to forte como em 1992 e 93. +O principal jogador da equipe o meia-direita Jos Basualdo. +Ele atuou pela seleo argentina na Copa do Estados Unidos. +Entre a defesa e o ataque, a bola quase sempre passa por seus ps. +A Llian sem calcinha fez mais estrago do que um bando do Comando Vermelho com revlver. +Ela uma profissional e conseguiu o objetivo dela. +Mas o presidente tem de ter postura. +O bom que o Fernando Henrique no estava presente e no se comportou como o Maurcio Corra. +S tenho um sentimento: estupefao. +Todo presidente uma autoridade pblica e deve cuidar da imagem daquilo que ele representa. +O fato do presidente ter posado ao lado de aquela atriz foi um vexame para toda a nao brasileira. +As letras do Digable Planets no refletem este radicalismo. +So mais cool, falam de amor, crescimento e amizade sempre com um vis engajado, porm. +O prprio nome da banda, segundo Ann Marie, vem de leituras sistemticas de Sartre, Marx, Kafka. +Significa planetas bacanas. +O nome seria uma tentativa de passar s pessoas o conceito de auto-suficincia cada ser um planeta mas tambm de inter-relao entre os planetas. +Da os nomes de insetos para ns, continua Ann Marie. +O atlas que a Folha vai distribuir aos leitores em 19 fascculos tem trs patrocinadores: o Banco Itamarati, a Companhia Brasileira de Alumnio (do grupo Votorantim) e a OAS Participaes. +No foi difcil encontrar patrocinadores para o projeto, segundo Antonio Carlos de Moura, 35, diretor comercial da empresa Folha da Manh S/A, que edita a Folha. +Uma mesa de encostar, em jacarand claro, feita mo no Rio no sculo 18, vai a leilo no prximo ms em So Paulo por US$ 15 mil, preo equivalente a dois carros populares zero quilmetro, como o Fusca. +O leilo ser na casa Renato Magalhes Gouva Escritrio de Arte, nos Jardins (zona oeste). +Colosio, candidato do Partido Revolucionrio Institucional (PRI, governista) foi morto a tiros em Tijuana durante um comcio. +Ele era o favorito para as eleies programadas para 21 de agosto. +Foi substitudo como candidato por Ernesto Zedillo, que chefiava sua campanha. +O percurso, que ser divulgado momentos antes da prova, deve incluir a avenida Bandeirantes, via Anchieta, represa Billings e chegada na av. Nove de Julho, em frente ao Banana Banana. +Ao final, todos os corredores comemoram a prova com uma feijoada na casa noturna. +Ali estar exposto o Lancer Evolution, carro de competio da Mitsubishi. +Ele acusado pela morte de pelo menos 83 pessoas e de ter ferido outras 200. +O Chacal chegou ontem s 10h30 (5h30 no Brasil) ao Palcio da Justia, usando algemas e cercado por um forte esquema de segurana. +SO PAULO A eleio presidencial est na sua pior fase, que aquela das articulaes polticas de bastidores, da costura sigilosa de alianas, das rasteiras e baixarias entre adversrios e correligionrios. + a fase do vale-tudo que antecede a escolha dos candidatos pelos partidos. + uma fase trgica para ns, eleitores. +Imagino que para a grande maioria dos polticos este seja o momento de glria porque exercita o que considera uma virtude: a arte da esperteza, a poltica segundo os cnones herdados da velha tradio das raposas polticas. +H quem se orgulhe dos golpes que consegue imaginar e confunda artimanhas com estratgia. + que os dirigentes petistas acreditavam tanto na vitria no primeiro turno que dispensaram os polticos, quer dizer, a direita. +Agora ela est de volta. +Jos Genoino, entrevistado, j falava como novo centro do poder. +Ns vamos colocar a campanha na rua, dizia, sorrindo. +A secesso est s comeando. +Maria Mole: mistura de bebidas, conhaque com martini e/ou vodka. +Muito N: coisa ou conversa negativa demais. +Futebol e poltica no se misturam. +Essa a postura que prevalece para a maioria dos jogadores da seleo brasileira de futebol. +Apenas o goleiro Gilmar (Flamengo) revelou o seu voto para as eleies presidenciais deste ano. +Gilmar pretende votar no senador Esperidio Amin (PPR-SC). +No quadro de hoje, voto nele, disse o goleiro do Flamengo. +Para governador ele ainda est indeciso. +s 21h15, o tucano Jos Serra, recm-eleito senador, juntou-se ao casal presidencial. +s 22h43, o governador eleito de So Paulo, Mrio Covas, e sua mulher Lila chegaram para o jantar. +Covas disse que Z Milionrio um velho amigo seu. +Jos Carlos Arajo foi scio de Octvio Cavalcanti Lacombe (morto em 1992), que fundou h mais de 30 anos a Paranapanema, companhia de explorao de minrios principalmente estanho e cassiterita na regio amaznica. +O ladro verificou qual era o saldo da conta de Maria Estela e, em seguida, retirou R$ 225 do caixa. +Antes de ir embora, o ladro pegou o ttulo de eleitor e o CIC da assistente de secretaria e os jogou atrs do caixa. +Ele disse que era para eu apanh-los e para no tentar reagir e impedir sua fuga, afirmou Maria Estela. +Mas, quando o ladro ia deixar o caixa, dois outros clientes do banco chegaram. +Arrelia foi o primeiro palhao brasileiro a ter um circo na televiso. +Era o Circo do Arrelia, na TV Record, em 1953. +Ele ficou famoso no todo o pas. +Arrelia se chama Valdemar e tem 87 anos. +Quando menino, gostava de estudar e no queria ser de circo, como seus pais. +Para Serra, se no for aprovada a emenda constitucional que desvincula dos fundos de participao dos Estados e municpios as receitas dos novos tributos, de nada adiantar o aumento de impostos. +Ainda no d para ficar tranquilo. +O prazo curto e os adversrios so muitos, disse Covas. +Para ele, a prxima semana ser decisiva para o governo, quando as MPs que tratam do aumento de impostos tero de ser necessariamente votadas. +Elas no podem ser reeditadas porque a criao ou alterao de impostos s podem ser feitas no ano anterior ao de sua cobrana. +O Banespa ainda no esclareceu as supostas dificuldades de uma agncia no exterior por haver pago parte das importaes sem licitao de equipamentos israelenses no governo Qurcia. +O BC no teria aprovado a operao. +Se o Ministrio Pblico Estadual comprovar danos ao patrimnio pblico com as importaes de Israel, os responsveis estaro sujeitos a ao de ressarcimento. +Mas o Concerto para Harpa, Obo e Orquestra de Cmara (1980) e sobretudo sua Chane 2 para violino e orquestra (1985) apontam para uma escuta mais livre, hedonista. +Resumindo: Lutoslawski radicalizou Bartk naquilo que este tinha de noturno, de dramtico e moderno. +Os acasos e interrupes misteriosas da msica noturna bartokiana se fizeram, em Lutoslawski, silncios e vibraes aleatrias da orquestra. +Jogos. +Os folhetos tero uma rede de distribuio indita at agora em qualquer campanha institucional do governo: carteiros da ECT (Empresa de Correios e Telgrafos), agncias e postos de atendimentos do Banco do Brasil, Caixa Econmica Federal, alm de postos lotricos. +Durante 15 dias, os bancos vo lhe dar reais em troca de cruzeiros reais. +Se for necessrio, esse prazo ser prorrogado. +Por isso, voc no precisa correr para trocar o seu dinheiro. +Essa a inscrio obrigatria nos folhetos e cartazes. +O atacante Casagrande aponta a falta de entrosamento como uma das causas para o seu baixo desempenho. +Sei que no estou rendendo tudo que posso, mas estou em evoluo, disse. +Folha -- O que voc acha das crticas que vem recebendo da torcida e da imprensa? +Segundo o mdico Flvio Pozzuto, as funes neurolgicas do torcedor se mantm sem alterao ele respira com ajuda de aparelhos. +O mdico diz que o edema neste caso irreversvel. +A leso foi grande e vai deixar sequelas. +A polcia pretende localizar todos os envolvidos na briga at o dia 21 de outubro. +O Conselho Monetrio Nacional aprovou ontem uma nova linha de emprstimos do Banco Central para socorrer bancos de qualquer porte. +Os emprstimos podero ser pagos em 90 dias e prorrogados pelo mesmo prazo. + a terceira linha especial de socorro criada pelo BC desde o lanamento do real, todas com o objetivo de atender bancos que tiveram sua sade financeira comprometida. +A frase do ex-presidente uma referncia a uma das promessas feitas por Cafeteira, um ex-sarneyzista que insinuava, durante a sua campanha, mandar Sarney para fora do seu Estado. +Se derrotar o candidato do PPR, Roseana se tornar a primeira governadora eleita na histria do pas. +No caso de impedimento do presidente da Repblica ou de vacncia do cargo nos primeiros dois anos de mandato, a substituio ser feita atravs de eleio direta em 90 dias. +Se a vacncia ocorresse nos ltimos dois anos de mandato, a eleio seria feita pelo Congresso, em 30 dias. +Nos dois casos, at a eleio a Presidncia seria exercida pelo presidente da Cmara dos Deputados. +Ficando novamente vago o cargo, o presidente do Senado e, sucessivamente, do Supremo Tribunal Federal (STF) assumem a incumbncia. +O nmero de aes ordinrias em junho apresentou queda de 10,36% comparado com o ms de maio. +Foram requeridas 606 aes em junho contra 676 no ms anterior. +Em relao ao mesmo ms do ano passado a queda ainda maior 34,98%. +A pesquisa feita pela Hubert Imveis e Administrao. +Em junho foram pedidas 145 aes revisionais de aluguel. +Comparadas com o ms anterior, as revisionais tiveram queda de 9,94%. +Porm, se o total de junho for comparado com o mesmo ms de 93, verifica-se aumento de 28,32%. +No 1 semestre de 94 foram feitos 662 pedidos no total. +Segundo o secretrio, US$ 43,5 milhes foram liberados pelo prefeito quatro meses atrs para ampliao ou reforma em dez hospitais e unidades de sade. +Entre elas, esto as reformas e ampliaes dos hospitais do Tatuap (US$ 7,4 milhes), Vila Nova Cachoeirinha (US$ 17 milhes) e Tide Setubal (US$ 9,4 milhes). +Raia disse ainda que sua equipe levantou as necessidades de material de consumo da rede e que US$ 3 milhes esto sendo liberados agora para gastos do primeiro trimestre. +Ao saber que os militares pretendiam a democracia e o fim da guerra colonial, os portugueses comearam a dar cravos aos soldados, que os colocavam na ponta dos seus fuzis -- da o nome Revoluo dos Cravos. +No processo que seguiu derrubada da ditadura, o poder caiu na rua. +Decises governamentais eram ultrapassadas pela fora de manifestaes. +A Secretaria Municipal da Administrao afirmou ontem que poder rever o decreto do prefeito Paulo Maluf que probe a contratao de portadores de doenas com possibilidade de se tornarem incapazes no futuro. +O decreto foi publicado no Dirio Oficial do dia 29 de julho. +No pargrafo nico do seu primeiro artigo, ele afirma que os candidatos no podem apresentar patologia com perspectiva presente, de incapacidade no futuro. +Os fatos esto a e so estarrecedores. +J passou da hora de governo, prestadores de servios e usurios se empenharem para no s aprofundar as discusses sobre a ineficcia do sistema de sade, como tambm para buscar alternativas e solues viveis, a fim de proporcionar um atendimento mais digno populao em geral. +A crise da sade tem soluo, basta vacin-la contra as ingerncias poltico-partidrias, as omisses e desacertos dos responsveis pelo setor. +O Supremo interpreta, em ltima instncia, a Constituio e as leis, mesmo que elas digam respeito a contratos privados. +Como poder faz-lo, porm, respeitando ele prprio a lei, quando existem legalmente duas moedas? +Como julgar em conscincia contratos escritos ou implcitos que dependem de uma moeda como meio de pagamento e de outra como unidade de conta? +A primeira varia tanto, dia-a-dia, que uma semana suficiente para produzir um aumento de mais de 10% nos vencimentos antecipados dos congressistas e dos magistrados. +Cabe perguntar: Se o Supremo no est de acordo com a suposta falta de regra das duas moedas, que existem de fato desde a emisso da ltima medida provisria, por que no a proclamou imediatamente inconstitucional e se serviu dela privadamente? +O Supremo Tribunal por acaso uma Justia privada ou o baluarte da Justia Pblica? +Como se v, trata-se de questes altamente perturbadoras. +Assim, se voc est usando uma planilha, por exemplo, pode selecionar parte (ou todo) e levar o que foi selecionado para outra planilha ou qualquer programa que estiver sendo usado. +Os arquivos das pastas tambm podem ser abertos a partir do menu, facilitando o uso de arquivos e aplicaes. +O operrio da construo civil negro e desempregado Rodney King, 28, vai receber US$ 3,8 milhes da Prefeitura de Los Angeles como compensao pelos efeitos da surra que levou de quatro policiais brancos em maro de 1991. +A absolvio em abril de 1992 dos policiais provocou 48 horas de conflitos raciais em LA que resultaram em 58 mortes, 4.000 feridos e US$ 1 bilho em prejuzos. +A contracultura, o movimento pelos direitos civis, a terceira onda gay e o politicamente correto nasceram aqui. +Ningum fica parado com o vento que sopra do Pacfico. +O politicamente correto institucionalizou-se, mas nem os americanos o suportam, exceto, claro, os eternos paroquiais. + uma nova religio. +A prpria Receita Federal pode ter, indiretamente, inspirado este golpe. +Quando o secretrio da Receita era Osires Lopes Filho, vrias cartas foram expedidas a profissionais liberais e empresas convidado-os a acertar as contas com o leo. +O delegado informa que foi aberta sindicncia interna no ministrio para tentar apurar o caso e que a Polcia Federal dever tambm abrir um inqurito. +Para avaliar as perspectivas deste ano, a Folha ouviu economistas e consultores que tm entre seus clientes muitas das principais empresas brasileiras. +Todos trabalham com trs cenrios: o provvel, o otimista e o pessimista. +De uma ponta a outra, as variaes so imensas e refletem a instabilidade de uma economia em inflao crnica e alta. +Para a taxa de inflao de dezembro de 1994, por exemplo, as previses variam de civilizados 2% para explosivos 150%. +Uma resposta est numa palavra: tranquilidade. +Num Mundial, esta palavra tem um significado determinante. +A tranquilidade pode levar uma equipe ao ttulo ou pode destro-la no caminho. +Num grupo de 30 pessoas, entre jogadores, tcnicos, mdicos e massagistas, que devem conviver durante tanto tempo, surgem problemas diariamente. +O McDonald's proibiu o cigarro nos 1.400 restaurantes de sua propriedade nos Estados Unidos. +Mais 3.600 concessionrios da marca aderiram proibio. +William Rhodes, vice-presidente mundial do Citibank, veio ao Brasil para reunio com executivos do banco. +Hoje deve se encontrar com FHC. +O problema grave. +Se no fizermos nada, pode haver uma tragdia. +O nosso jogo aqui preveno, disse o secretrio de Segurana Pblica do Estado de So Paulo, Antnio Correia Meyer. +O secretrio comandou ontem uma reunio sobre segurana nos estdios, na sede da Federao Paulista de Futebol. +Estiveram presentes dirigentes do futebol, e autoridades policiais civis e militares. +Cornlio Pires -- Folclorista e contador de causos (1884-1958), foi o primeiro produtor independente de discos do Brasil. +Raul Torres & Florncio -- Dupla que atuou dos anos 40 aos 60 e criou um estilo romntico de interpretao. +Reis no quis dizer se tambm abandonar Esperidio Amin. +Um acidente envolvendo um caminho e um nibus, na ltima sexta-feira, matou 16 estudantes, trs motoristas e deixou nove feridos no km 5,4 da estrada, que chamada de rodovia da morte. +De acordo com a assessoria, o secretrio dos Transportes, Mrcio Ribeiro, determinou que os engenheiros do Departamento de Estradas de Rodagem (DER) apressassem os estudos para a duplicao. +Crianas norte-americanas encontram em maio mais dois programas educativos em CD-ROM. +Through the Woods (Pelo Bosque) e At the Seashore (Na Praia) foram desenvolvidos pela IBM. +Through the Woods voltado para crianas que esto no primeiro ano do 1 grau. +Cada um dos produtos deve custar US$ 329. +Incluem ainda um outro CD, que serve como guia para uso em sala de aula. +A maior parte da produo (25O t) j est comprometida com uma indstria de processamento da polpa. +O restante ser comercializado em supermercados, feiras e indstrias de sorvetes do Paran. +O fazendeiro, que cultiva tambm 1 hectare de rosas, diz que optou pelo morango aps experincias nem sempre vantajosas com outras culturas. +Eu tenho um projeto, que vou revelar aqui pela primeira vez, que tem a assinatura de um dos homens mais inteligentes e criativos do mundo, Eliezer Batista. +Ele est me municiando e me dando o instrumental de que careo para imaginar a soluo dos problemas do Estado. +Schumacher era a esperana de conferir alguma emoo a um Mundial que j parecia ter dono. +E emoo significa pblico, o qual atrai patrocinadores. +Depois, com a morte de Senna, vieram as exigncias por medidas de segurana na F-1. +Mas a bordo de um 'cadillac' preto conversvel, o presidente Menem atravessou a pista do parque da Sociedade Rural Argentina sob os aplausos da platia de cerca de 10 mil agricultores. +No chegamos ao paraso, mas j conseguimos sair do inferno, disse Carlos Menem da tribuna de Palermo. +Ontem, os bancos j se adequaram s novas regras. +Todas as linhas foram travadas em trs meses, disse Mrio Luna, gerente de Departamento de Crdito e Financiamento do Bradesco. +Luna afirmou que cedo para fazer uma avaliao completa do impacto das novas medidas. +O tcnico Jair Pereira deve escalar um time de reservas amanh, s 17h, contra o Flamengo, em Caio Martins. +Quero todos totalmente recuperados para as quartas-de-final, afirmou o treinador corintiano. +Ele considerou surpreendente o desempenho do russo Alexander Popov. +O ex-jogador defendeu tambm a adoo de penas para menores de 18 anos que cometam crimes. +Ningum, por mais criana que seja, pode ir para o estdio com uma arma. +A participao aumentaria 3% at meados de 95. +O trabalho da publicao inglesa, divulgado pelo Financial Times, indica que h menos preocupao com riscos polticos e econmicos do que com a falta de liquidez e diversificao. +No caso de Alvarez, a intimidade com os EUA levou-o a conhecer a mentalidade norte-americana, seus gostos, seus costumes. +De certa forma, afiou-o para a luta em que se engajaria a partir dos anos 60. + desse combate, da maneira como ele chegou a criar as formas que geraram uma obra original, que O Olho da Revoluo tira seu interesse. +O livro chega num momento em que a idia de cinema engajado est meio por baixo. +Cuba tambm est com a imagem um tanto abalada (crise, xodo por mar etc). +Mas justamente por isso O Olho da Revoluo chega num momento apropriado. + perto de casa e tranquilo, diz Rossi, em meio a goles de refrigerante em lata, dentro de seu Opala estacionado atrs de uma igreja. +Folha -- O estado de stio seria um golpe? +Serra -- Seria e o foi apresentado assim, numa reunio da qual participei. +Nela, o presidente confidenciou no acreditar mais que terminaria o seu mandato. +Foi seis meses antes do golpe. +As dificuldades que bancos federais e estaduais provavelmente enfrentaro reforam as dvidas quanto convenincia do setor pblico ser proprietrio de tantas instituies financeiras. +O Brasil dispe de um dos mais modernos sistemas bancrios privados do mundo. +O fato de que os bancos pblicos venham a ser, recorrentemente, fontes de prejuzo vem s reforar as desvantagens da atuao do Estado nesse setor. +Na Bblia, h duas histrias que tentam explicar a origem de tantas lnguas. +Essas histrias so chamadas de mitos. +A histria da Torre de Babel (Gnesis, captulo 11, no Antigo Testamento) diz que a Terra tinha uma s lngua e um s modo de falar. +Ento, os homens encontraram um lugar plano e resolveram construir uma torre que chegasse at o cu. +Trata-se de uma estratgia de inflao reprimida, pronta para rebelar-se pela via da exploso cambial no momento em que os agentes econmicos se convencem de que no sustentvel, e em que os capitais comeam a fugir. +Ela dever inviabilizar a atual oportunidade de estabilizar afetivamente a economia brasileira e, de quebra, dever fazer uma srie de estragos irreversveis, que em muito debilitaro seu potencial de desenvolvimento a longo prazo. +O PRN no tem mais candidato Presidncia da Repblica. +O partido expulsou ontem Walter Queiroz por considerar que ele faltou com a verdade no episdio da dupla declarao de renda. +Depois de registrado, Queiroz apresentou uma segunda declarao ao TSE (Tribunal Superior Eleitoral), acrescentando bens que alegou ter esquecido nos primeiros papis que foram apresentados. +No h diferena essencial entre as armas individuais do trfico e as do Exrcito. +Elas pertencem basicamente categoria fuzil automtico, isto , um hbrido surgido na Segunda Guerra entre os tradicionais fuzis e metralhadoras de mo ou submetralhadoras. +Fuzis disparam munio mais poderosa e a um alcance maior, de at um quilmetro. +J as submetralhadoras disparam balas semelhantes s dos revlveres, mas podem faz-lo em rajadas, a um alcance curto, de dezenas de metros. +Nesta ala merece meno o seu Bruto Ros, um espumante elaborado com uva Baga (100%) que conserva um pouco da cor e muito de frutado desta variedade tpica da regio. +Este um vinho bem seco e encorpado capaz de escoltar uma refeio completa. +O de 91, tem bolhas finas, abundantes e persistentes, que lhe do um paladar cremoso marcado como o aroma por um toque atraente de cascas ctricas e framboesa. +J no captulo dos goles decididamente rubros, desponta o Joo Pato (75% Baga, 25% Cabernet Sauvignon). +Quem experimenta a ltima verso (safra 91) descobrir um vinho novo ainda, adstringente, mas de aroma e sabor intensos. +Um vinho marcado pelo belo contraponto entre a especiaria da Baga e as frutas vermelhas da Cabernet, quase uma marca registrada na criao deste vinicultor portugus. +A atacante Ana Moser (26 anos, 1,85 m e 70 kg), revelou ontem que, depois do vlei, sua segunda paixo o futebol. +Quando era menina gostava muito de jogar futebol com meu irmo, disse a jogadora, que, ao contrrio do vlei, onde atua como atacante, prefere jogar como quarto-zagueiro no futebol. +Anteontem, o Banerj, que administra a dvida pblica estadual, no conseguiu que o Banco Central negociasse R$ 84 milhes em ttulos estaduais, trocando-os por ttulos federais. + o que se chama de alavancagem com um emprstimo de US$ 10, por exemplo, pode-se entrar num jogo de US$ 100 ou mais. +Se a aposta for errada, o risco de bancarrota uma forte possibilidade. +A expanso dos derivativos no foi gratuita. +Os economistas alinham vrios motivos bsicos, entre eles a possibilidade de inverso aberta com a queda dos regimes socialistas, a onda liberalizante que varre a Amrica Latina, a forte expanso econmica dos pases do Sudeste Asitico e tambm a baixa e estvel remunerao de tradicionais aplicaes. +Nos EUA, os CDBs, at antes da alta dos juros, rendiam 3% ao ano para uma inflao de 2,5%. +O IRA (Exrcito Republicano Irlands, separatista) admitiu o envolvimento de seus militantes no assassinato de um funcionrio do correio durante um roubo na cidade de Newry (Irlanda do Norte). +No ltimo dia 11, Frank Kerr, 53, foi morto com um tiro na cabea quando trs homens assaltaram um posto do correio. +Os trs levaram cerca de US$ 210 mil. +Eles so a razo de tudo o que fao, de tudo aquilo em que acredito, disse, com grandes espaos entre as palavras. +Tentava, a custo, firmar a voz, sob flashes dos fotgrafos. +Ricupero, na verdade, foi o ltimo do grupo que o acompanhava familiares e assessores a sucumbir emoo, pelo menos publicamente. +Vice de FHC, Maciel nunca usou tanto a expresso ternurar. +Ele a emprega quando tem que dizer no a uma pessoa sem desagrad-la no caso, a seus colegas de PFL que querem repartir os cargos com o PSDB antes do 1 turno. +Quem adere depois da vitria fisiolgico. +Como melhorar o ensino superior sem transformar as universidades em escoles, onde os professores s repetem e se afastam da pesquisa? +Essa foi a principal questo do 1 dia de debate, do qual participaram os professores Jos Arthur Giannotti (Departamento de Filosofia da USP), Jos Augusto Guilhon Albuquerque, (Departamento de Cincia Poltica da USP), e Luiz Pinguelli Rosa (diretor da Coordenao de Programas de Ps-Graduao da Universidade Federal do Rio de Janeiro). +O bom uso dos modems e fax/modem depende dos programas para gerenciamento e transmisso de dados que acompanham os equipamentos. +Todos os fax/modem saem de fbrica com dois programas um para gerenciamento das transmisses dfa e outro para gerenciamento das transmisses de dados. +Na delegacia, que fica no Jardim Mutinga trabalham seis policiais com dois carros. +A Caravan foi queimada. +Resta um Gol. +Os outros carros incendiados so trs Passats, um Chevette, dois Fuscas e um Escort. +Somos quercistas no atual momento. +Ele desenvolvimentista, defende a nao soberana contra o imperialismo, representa as foras populares. +Temos vrtebra poltica, no somos gelia. +O PT no tem estatura ideolgica para enfrentar o candidato conservador. +Por isso est como cego em tiroteio. +Pesquisa da Cia. City de Desenvolvimento mostra que os empresrios procuram locais com infra-estrutura, facilidade de acesso, boa oferta de mo-de-obra, prximos de So Paulo e das rodovias e longe de movimentos sindicais. +Temos problemas com clientes e fornecedores que no conseguem encontrar a empresa, afirma Srgio Ueta, gerente administrativo da Ueta Indstria e Comrcio de Aparelhos Eletrnicos. +A empresa, com sede em Caieiras (35 km a norte de SP), pretende se mudar para o Empresarial Jaragu, onde adquiriu 6.000 m2. +Ao mesmo tempo o atual papado esmagou a teologia da libertao, teorias de concorrncia feroz so implantadas em toda parte, o fascismo est de volta, Fidel Castro se tornou uma espcie de Somoza e at Martin Luther King, ficou provado, era plagirio. +No plano dos costumes, a reviravolta se apresenta sob aspecto viral, epidmico. +Os outrora odiosos valores de famlia, sucesso e consumo renascem, sobretudo entre a populao jovem. +Sexo livre, fumo e droga esto condenados. +O narcisismo assume uma feio cada vez mais frvola. +O substitutivo d aos ministrios da Fazenda e Justia poder de pedir interveno judicial nas empresas acusadas de praticar aumento injustificado de preos. +Os senadores que faltarem s sesses do Congresso revisor, a partir de hoje, tero cortes no valor de um dia de trabalho por falta. +O desconto seria de CR$ 83 mil por dia, com base nos vencimentos de fevereiro. +A deciso foi anunciada na tarde de ontem pelo presidente da reviso constitucional, senador Humberto Lucena (PMDB-PB), que tomou a deciso mesmo tendo em mos parecer contrrio da sua assessoria jurdica. +Lucena se declarou pressionado pela imprensa e pela necessidade de buscar o qurum nas sesses da reviso. +Segundo o senador, a deciso quanto ao corte dos salrios dos deputados a deciso cabe ao presidente da Cmara, deputado Inocncio de Oliveira (PMDB-PE). +Inocncio j havia anunciado a adoo do corte de um 30 avos dos salrios dos deputados por falta em sesso da Cmara. +Os primeiros descontos aconteceriam na folha de pagamento deste ms, que se encerra dia 15. +At as 20h, ele no havia se pronunciado sobre a extenso da medida tambm aos trabalhos da reviso. +O vice-presidente da Cmara, Adylson Motta, disse que o regimento da Cmara impede o corte. +Favorveis ao pressuposto da integrao, devemos ser criteriosos, no descartando e nem acatando, de pronto, tudo o que nos oferece o governo federal. +Os exemplos de Simon e Brizola revelam que a precipitao e a busca de resultados polticos imediatos constrem o pior caminho para o desenvolvimento e so vcios que o curso da histria no tarda a desnudar. +RICARDO ANTNIO SILVA SEITENFUS, 45, doutor em Relaes Internacionais pelo Instituto de Altos Estudos Internacionais em Genebra, coordenador do curso de mestrado em Integrao Latino-americana da Universidade Federal de Santa Maria (RS). +Foi secretrio especial para Assuntos Internacionais do Rio Grande do Sul (governo Pedro Simon). +O avio em que o presidente da Argentina, Carlos Menem, viajava no ltimo dia 30 de dezembro para Anillaco, sua cidade natal, iria ser atacado por um mssil, disse ontem o jornal de Buenos Aires Ambito Financiero. +A segurana teria sido avisada por dilomatas e desviado a rota do avio. +O governo nega a suposta tentativa de atentado. +Comearam ontem pela manh em Nova York os desfiles de prt--porter outono-inverno. +A primeira estilista foi a Donna Karan, com a linha DKNY. +Os desfiles se realizam pelo segundo ano em tendas ou pavilhes localizados no Bryant Park, que fica atrs da Biblioteca Nacional, na rua 40. +Deputados ligados a ACM diziam ontem, em conversas com colegas, que Benito Gama (PFL-BA) foi vetado para ser vice de FHC porque o grupo avalia que so cada vez mais reduzidas as chances de vitria do tucano. +Ontem, vrios operadores da Bovespa creditavam a queda do ndice da bolsa percepo do mercado financeiro de que o PFL leia-se ACM est desembarcando da candidatura de FHC. +As constantes desavenas com seu pai, John Paul Getty, fizeram John Paul Getty 2 entrar no affaire. +As Trs Graas. +A clebre escultura de Antonio Canova, atualmente na Gr-Bretanha, est sendo cobiada pelo museu Getty, de Malibu, na Califrnia. +Hoje a mdia brasileira de cerca de 1.800 quilos por hectare. +Segundo os produtores, o incentivo pesquisa elevar este desempenho para 2.400 kg/ha. +As possibilidades do trigo irrigado tambm so animadoras. +O plantio no Cerrado atingiu 5.000 kg/ha nas lavouras comerciais e mais de 8.000 kg/ha em reas experimentais. +Os quatro grandes jornais deram, na segunda-feira, a mesma manchete para informar como acabou o Grande Prmio do Brasil, que aconteceu domingo passado. +Era Senna erra e Schumacher vence (em O Globo, a frase ficou pouco maior que isso). +Com essa constatao enviesada de que o alemo s chegou em primeiro porque o brasileiro cometeu uma bobagem, a imprensa coroou o festival sennista da semana anterior, quando todas -- sem exceo, todas -- as coberturas apontavam a vitria de Ayrton Senna em Interlagos. +A Folha chegou a escrever que o melhor piloto do mundo (Senna), na direo do melhor carro do mundo (a Williams) teria sua consagrao no autdromo. +E que s Schumacher acreditava em sua possibilidade de vencer. +Senna, todo mundo viu, rodou quando estava em segundo lugar e com poucas chances de recuperar o primeiro. +Enterrou as previses mais do que otimistas, e deixou ver quanto viciado o noticirio esportivo. +O compromisso com a preciso (e, por extenso, com o leitor) vale menos do que a torcida da imprensa nessas horas. +A imprensa j se esqueceu de que os oligoplios so o vilo da URV e do plano FHC. +O pas convive com o novo indexador h um ms, o ministro virou candidato e os preos chamados de abusivos continuam em vigor. +O assunto saiu da pauta, ainda que esteja dentro do bolso do leitor. +JUNIA NOGUEIRA DE S a ombudsman da Folha. +A ombudsman tem mandato de um ano, renovvel por mais um ano. +Ela no pode ser demitida durante o exerccio do cargo e tem estabilidade por um ano aps o exerccio da funo. +Suas atribuies so criticar o jornal sob a perspectiva do leitor recebendo e checando as reclamaes que ele encaminha Redao e comentar, aos domingos, o noticirio dos meios de comunicao. +Cartas devem ser enviadas para a al. Baro de Limeira, 425, 8 andar, So Paulo (SP), CEP 01202-001, a. c. Junia Nogueira de S/Ombudsman. +Para contatos telefnicos, ligue (011) 224-3896 entre 14h e 18h, de segunda a sexta-feira. +No ranking dedicado aos homens aparecem o ator Sean Connery e o cantor Sting. +Entre os corpos mais feios est o da top-model Kate Moss, que segundo o jornal tem ombros cados e o peito liso. +Os deputados que negociam o plano de estabilizao econmica do governo criticaram o ministro Fernando Henrique Cardoso, que admitiu ontem a possibilidade de disputar a Presidncia da Repblica. +A declarao do ministro repercutiu mal tambm entre os lderes polticos no Congresso. +A frase do ministro desagradou at mesmo ao PSDB. +Para o plano econmico, no ter dito essa frase seria melhor, afirmou o senador Beni Veras (PSDB-CE). +O So Paulo comeou o primeiro tempo aptico, mas acordou aos 10min com uma falta no marcada do lateral Mac Allister em Euller, quando este ia entrar na rea. +Em seguida, Euller fez mais duas jogadas. +A partir dos 22min, o So Paulo passou a marcar sob presso e anulou o adversrio. +O Boca no conseguia sair jogando e perdia bolas em seu campo. +O autor do ataque seria Nagi Mohamed Mustafa e o lder do grupo seria Basseem Khalil, morto num caf do Cairo (capital) aps tiroteio contra a polcia. +O atentado teria sido planejado pelo grupo radical islmico Gama'a al-Islamiya, que lanou campanha contra o governo egpcio em 1992 para a criao de um Estado Islmico fundamentalista. +Khalil e Mustafa, disfarados, tentaram matar Mahfouz um dia antes, mas ele no estava em casa. +Muitos negcios foram iniciados ou fechados durante o Siaf. +Produtores da Paraba, por exemplo, venderam abacaxi a um grupo de empresrios espanhis, no valor de US$ 323 mil. +A cooperativa de Cura (BA), no Vale do So Francisco, praticamente acertou uma joint venture com um empresrio portugus para produzir melo e uva. +Empresrios alemes avanaram nas negociaes para instalar unidades de beneficiamento de polpas de frutas. +Pode-se dizer, em outras palavras, e a grosso modo, que o aluguel do primeiro ms de reajuste sempre embute (por estimativa) uma inflao que reduzir o aluguel nominal ao seu valor real na metade do perodo de reajuste subsequente. +Portanto, na primeira metade, o locador sai ganhando, pois recebe mais aluguel do que vale a locao. +Na segunda metade, a vantagem do inquilino que papa menos aluguel do que vale o uso do imvel. +A mdia no perodo de reajuste equilibra as vantagens de cada contratante durante todo perodo de uniformidade nominal do aluguel. +Tudo isso deve ser considerado para que se entenda que uma exata transposio do aluguel praticado em cruzeiros reais para o sistema da URV s possvel se os contratantes procederem a uma extrao da mdia dos aluguis nos ltimos meses que corresponderem a periodicidade de reajustamento do contrato, para, s ento, transformar tal mdia em URVs. +Folha -- No caso de Vereda da Salvao, como foi esse trabalho conjunto? +Serroni -- A pea previa trs casas numa clareira de uma floresta. +Depois de conversar com Antunes, sempre fao uma maquete. +Como a pea trata de religio, de misticismo, achamos que devamos usar muitos troncos, dar uma verticalidade, o que tem a ver com a ascenso etc. +Decidimos ento, por uma questo de economia cnica, abolir uma das casas e s insinuar as outras duas, fazendo apenas suas entradas, que parecem as de um templo. +Isto foi fundamental, pois conseguimos aumentar nossos criatrios e agora usamos como garanhes selecionadores apenas aqueles realmente de ponta, diz. +O resultado positivo da estratgia pode ser comprovado agora, dizem os criadores, uma vez que hoje so os EUA que esto buscando cavalo rabe no Brasil. +Os franqueados brasileiros comeam a utilizar uma nova arma para aumentar seu poder de influncia junto cpula das empresas franqueadoras. +Trata-se do conselho de franqueados, que comea a surgir em algumas redes como McDonald's, gua de Cheiro, Multicoisas e Localiza. +J bastante difundidos nos EUA, os conselhos tm atenuado problemas e melhorado o tradicionalmente difcil relacionamento franqueado X franqueador. +Atuando em conjunto, os franqueados ganham fora e passam a ter voz ativa em questes vitais para o negcio, como definico de produtos, prazos de pagamento e estratgias de propaganda. +A liminar, ainda no cumprida pelo governo do Esprito Santo, determina que o delegado responsvel pelo caso, Francisco Badenes, seja reconduzido ao cargo. +Em maio ltimo, Badenes foi afastado e transferido para o interior. +O juiz determina ainda o restabelecimento dos meios necessrios ao funcionamento da Comisso de Processos Administrativos Especiais, responsvel pelo inqurito. +O lder Tasso Jereissati (PSDB) passou de 61% para 58%, perdendo trs pontos percentuais em relao ltima pesquisa. +No deve haver segundo turno no Estado. +Wilson Barbosa (PMDB) subiu seis pontos, atingindo 55%. +O candidato tambm deve vencer a eleio no primeiro turno. +A margem de erro da pesquisa Datafolha de 3,0 pontos percentuais, para mais ou para menos, exceto no Distrito Federal, que de 4,0 pontos. +A direo do datafolha dos socilogos Antonio Manuel Teixeira Mendes e Gustavo Venturi. +Nada do que eu havia lido ou ouvido antes de vir para c me preparara para as emoes que eu iria viver e vrias vezes me senti numa montanha-russa emocional. +Em vrias ocasies eu me vi, como anotei em meu dirio, beira das lgrimas. +Mas em muitos outros momentos eu me esforava para conter risadas. +Ou ento lutava contra uma depresso repentina, tentava controlar minha raiva, fazia piadas com algum ou fazia fora para no sentir saudades de casa. +Art. 1 Dispensar a obrigatoriedade da expresso de valores em cruzeiros reais constante dos incisos II e III, do art. 8, da medida provisria n 482, de 29 de abril de 1994, desde que, no caso de fixao dos preos em Unidade Real de Valor (URV) seja exposto, em lugar visvel e de fcil leitura, o valor da URV do dia. +Art. 2 obrigatria a expresso dos valores em cruzeiros reais nas notas fiscais. +As negociaes com os petebistas se prolongaram at o incio da madrugada de ontem. +Alm do PSDB e do PFL, coligou-se em torno da candidatura Covas o pequeno PV. +Mas at possvel encontrar pelo menos um ponto interessante no disco: algumas de suas letras, compostas por Michael Callahan e Marc English. +Na faixa mais interessante do disco, Wooden Nails, a primeira, o tema abordado a superao da dependncia de drogas e da depresso. +Uma melodia melanclica emoldura a cano. +O fato que a sombra do R.E.M. uma presena muito forte nesse Building Our House. +Os vocais de Callahan e English so calcados ao extremo nos de Michael Stipe e Mike Mills. +O beat de certas canes acaba tornando-as filhotes das compostas pela banda de Athens, como Losing My Religion. +Alm disso, deve lanar mais cinco novidades at o final do ano, alm de aumentar sua distribuio. +Apesar dos esforos, estima fechar o ano vendendo 75 milhes de litros de sorvetes (US$ 120 milhes). +Em 93 vendeu 83 milhes de litros (US$ 128 milhes). +Ou na hiptese remota de sua votao antes de 30 de agosto. +Para Holanda, no possvel a mudana pretendida pelos bancos. +Por dois motivos: a eventual queda na arrecadao e problemas jurdicos que impedem mexer na base de clculo do imposto por meio de MP, j que ela foi fixada em emenda constitucional. +O governador do Paran ligado politicamente ao ex-governador Roberto Requio que candidato ao Senado pelo PMDB. +Requio um dos mais ferrenhos adversrios de Orestes Qurcia, que disputa a Presidncia da Repblica pelo partido. +Enfim, nem tudo est perdido neste futebol automatizado e excessivamnete defensivo que a Copa est consagrando. +Afinal, por mais que tentem matar o craque, ele sempre sobrevive, aqui ou ali, onde menos se espera. +Alberto Helena Jr., 52, colunista da Folha. +Sua coluna na Copa diria. +O presidente argentino, Carlos Menem, disse que lanar sua candidatura para a eleio de 2003 se for eleito em 1995. +Depois de dois mandatos, Menem no poderia ser reeleito em 1999. +Espero quatro anos e volto a me apresentar, disse. +Duas bombas mataram duas pessoas em Teer ontem, segundo a agncia oficial de notcias Irna. +No foram apontados suspeitos pelo atentado embora tenha sido dito que a polcia achou pistas dos terroristas. +A esfora incidia sobre o valor das propriedades, as quais estavam devidamente registradas nos diagramas pblicos. +Tratava-se de um imposto sobre o capital. +Outro tributo importante, e tambm sob a forma de contribuio voluntria, foram as trierarquias. +Tratavam-se de contribuies para construir os geis barcos trirremes. +Tanto a construo quanto a manuteno da equipagem corria por conta dos que tinham mais recursos. +O Movimento Zona Sul foi criado no ano passado, durante a polmica a respeito da realizao das obras de prolongamento da avenida Faria Lima (zona sul). +Comandado pelo advogado Luiz Antonio Siqueira Dias, o grupo defendeu as obras, que foram iniciadas recentemente. +O Folha Informaes atendeu no ltimo fim-de-semana 3.978 pessoas, que acionaram o servio para saber o resultado de Guarani e Palmeiras. +O pico de audincia aconteceu entre as 20h e 21h de sbado, quando o sistema atendeu 309 ligaes. +O Folha Informaes um servio do Banco de Dados da Folha de S. Paulo (tecnologia da Telesis Sistemas em Telecomunicaes). +Nenhum dirigente do PMDB compareceu ontem, em So Paulo, cerimnia de instalao do Instituto Ulysses Guimares de Estudos Polticos e Sociais. +Ulysses morreu em 1992 como presidente nacional do partido. +Orestes Qurcia e Luiz Antonio Fleury Filho no estiveram na sede da entidade. +Foi tambm o caso de candidatos a cargos majoritrios ou dirigentes regionais. +Os carros ganharam ainda proteo sob o crter e sob o tanque de gasolina, para suportar com menor risco os choques contra as pedras. +A altura do solo foi ampliada em alguns centmetros para facilitar as investidas fora de estrada. +A crise no Mxico deve fazer com que todos os pases emergentes recebam um menor volume de capitais externos de renda fixa (aplicados, por exemplo, em eurobnus). +Ele acredita, no entanto, que o mercado acionrio brasileiro deve crescer em 1995. +A instituio responsvel pela guarda (custdia) do equivalente a US$ 3,7 bilhes de investimentos estrangeiros nas Bolsas. +Nos ltimos dias, houve uma sada de US$ 100 milhes da custdia da instituio, por causa da crise do Mxico. +Meirelles afirma que esses recursos no saram de negcios tradicionais de Bolsas, mas sim de operaes de box (financiamentos tendo como lastro aes). +Ele prev, contudo, que o mercado acionrio crescer e a custdia de aes de investidores estrangeiros na instituio chegar a US$ 5,5 bilhes ao final de 1995. +Apesar de evitar dar um no definitivo, Marise deixou claro que deve recusar o convite de Brizola. +Fiquei muito honrada com a proposta, mas o governador Brizola tem nomes muito bons para compor sua chapa, afirmou. +O governo do Estado informou, atravs de nota de sua assessoria de imprensa, que as declaraes do presidente Itamar Franco e do presidente eleito Fernando Henrique Cardoso sobre medidas federais de combate ao trfico de drogas configuram posies anlogas s que o governador do Estado vem defendendo. +Segundo a nota, as providncias capazes de alterar o quadro atual de combate aos traficantes dizem respeito Polcia Federal e s Foras Armadas, com o objetivo de interromper o fluxo de armas proibidas e cocana no Rio. +Confiante numa eventual vitria do candidato Fernando Henrique Cardoso, a cpula do PSDB j articula a formao de um superpartido para no depender do PFL no Congresso. +Os tucanos consideram perigoso deixar um possvel governo de FHC sujeito s manobras dos deputados e senadores pefelistas. +Grava disse que os dois sofreram rompimentos de feixes musculares em msculos da coxa direita. +O problema de Viola num msculo da parte de trs da coxa e de Clio est no lado interno. +O mdico negou que esteja tratando mais algum. +O Cobra privilegiava os atos impulsivos e as cores fortes e seus principais nomes foram Appel e Alechinsky. +Doucet se inspirava muito em suas viagens e refletia isso no ttulo de vrios de seus trabalhos, como Guatemala Blues e Mostar Sarajevo. +Tambm so obras suas Labyrinthe de la Lumire e Turbulences d'Abysses. +O candidato do PDT disse que quer fazer o mesmo. +Os grandes comcios sero realizados apenas em setembro. +Ao contrrio do que fez em campanhas anteriores, Brizola quer promover comcios rpidos, com trs oradores. +O filme passa como vento pelas questes. nada pequenas. dos tratamentos psiquitricos contemporneos e se fixa no que existe de mais bvio na histria. +Isso termina por arrast-la destestavelmente, sem que se chegue a parte alguma. +Paradoxalmente, por a que se podem ver as virtudes de Figgis. +A ONU admitiu que os srvios no haviam cumprido as exigncias, mas recusou autorizao para bombardeio. +Yasuhi Akashi, reprsentante da organizao, argumentou que os srvios estavam em meio ao processo de retirada. +J nas primeiras palavras a Medida Provisria que institui a Unidade Real de Valor incorre em erro e abre brecha para contestaes na Justia. +A constatao do jurista Saulo Ramos, ex-consultor Geral da Repblica e ex-ministro da Justia, que foi convidado a analisar o rascunho das medidas e na ocasio alertou para dois problemas: o artigo primeiro e o pargrafo primeiro da MP 433 praticamente instituem duas moedas no pas, o que proibido. +Alm disto, a manuteno da Ufir para correo de tributos ilegal, segundo o jurista. +Outros especialistas apontam mais defeitos tcnicos na MP. +Diz o artigo primeiro que fica instituda a URV, dotada de curso legal para servir exclusivamente como padro de valor monetrio. +Isto restringe URV todos os poderes da moeda do pas. + um erro tcnico grave e que pode resultar num conflito judicirio muito grande, segundo Saulo Ramos. +As negociaes para a volta de Mansell F-1 tiveram incio logo aps as 500 Milhas de Indianpolis, realizada no ms passado, e continuaram durante as 200 Milhas de Milwaukee, disputada no ltimo dia 5, quando recebeu a visita de alguns engenheiros da Williams. +Quinto colocado no Mundial de F-Indy, com 46 pontos, Mansell disse que pretende continuar a correr na categoria at o final da temporada, condio que ele imps a Frank Williams para voltar equipe pela qual ganhou o ttulo mundial de 1992. +Nos ltimos dias, aumentaram as especulaes sobre quem ocuparia o lugar de Mansell na Newmann/Hass. +O mais cotado na bolsa de apostas o brasileiro Raul Boesel, que ontem no consegui terminar a corrida em Detroit. +Eles estranharam o fato dele no sair do barraco de manh e chamaram a polcia. +Jair morava sozinho no barraco e, segundo seus vizinhos, era alcolatra. +O segundo caso aconteceu no Pari (centro). +Um indigente conhecido como Daniel foi encontrado cado na rua Canind na madrugada de ontem por policiais que estavam fazendo patrulhamento. +Quem que vai, ento? +S intelectual quatro-olhos? +S aquela turma do Cebrap? +S socilogo e antroplogo? +Socorro! +Ento bom a Joyce Pascowitch ir se preparando. +O Fernando Collor, evidentemente, no foi convidado para a posse. +So what, no mesmo? +Ele compensou em Aspen, dando um malho naquela flor de nome Rosane, casualmente, na frente de fotgrafos. +Na alquimia medieval, a rvore simbolizava a transformao, sempre cercada de globos reluzentes que corresponderiam aos planetas. +Para o psicanalista Carl Jung, esta seria a origem dos globos pendurados nas rvores contemporneas. +E, para Bruno Bettelheim, as pequenas velas e lmpadas que a iluminam seriam vestgios das antigas fogueiras que os pagos do norte da Europa ateavam no alto das montanhas para antecipar a chegada do Sol e o fim do inverno. +Mais 10%. +Para 94, a indstria de mquinas agrcolas prev crescimento de 10%. +Acordo na Cmara Setorial garante crdito de US$ 650 milhes para o Finame Rural. +Em contrapartida, a proporo de empresrios no partido, que era de 22%, hoje alcana a marca de 54%. + o caso, entre outros, do deputado Srgio Machado (Villejack Jeans), do senador Teotnio Vilela Filho (Mata Verde Agropecurio, Usina Seresta), do senador Albano Franco (presidente da Confederao Nacional da Indstria). +Essa mudana no perfil social da bancada no ocorreu no maior partido conservador, o PFL, nem no PT, partido de esquerda. +O produtor iniciou a colheita, em fevereiro passado, de 170 hectares de amendoim e 240 hectares de soja. +Em 93, ele plantou 290 ha de amendoim e 97 ha de soja. +Com o dinheiro do amendoim Guidi conseguiu comprar duas camionetes e um trator de 150 Hp, est construindo uma casa em Pontal e ainda tem para receber US$ 470 mil referente venda de 49,4 mil sacas. +Parte desse dinheiro vai para a colheita deste ano. +Trabalho com amendoim h 27 anos e sempre tive lucro. +Nos primeiros dez anos, o lucro por safra alcanava 40%. +Hoje, varia entre 12% e 15%. +O Banco apresentou em 1993 um nvel de alavancagem alto, demonstrando maior agressividade nas suas operaes. +Os nveis dos depsitos aumentaram 440%, em 1993, basicamente em funo de depsitos interfinanceiros e a prazo. +MRIO ALBERTO DIAS LOPES COELHO consultor da Austin Asis. +Existe ambiente poltico, social e econmico para se produzir automveis no Brasil? +Sim, embora a abertura dos portos no esteja sendo feita impunemente. +Em termos polticos, o pas saiu das eleies com um Estado mais vigoroso e democrtico, em melhores condies de enfrentar a inflao e o desemprego. +Pode se reorientar para uma maior produo industrial e contribuir para a criao de mais emprego e riqueza, consequncia natural do processo. +Do ponto de vista lgico, um plano de estabilizao equivale a uma promessa. +O governo promete sociedade que, de agora em diante, vai mudar de vida vai garantir o equilbrio das contas pblicas e vai parar de abusar do seu monoplio de criao de moeda. +Se isso ser cumprido na prtica, s o tempo dir. +Mas, para que a promessa possa ser efetivamente cumprida, fundamental que se acredite que o governo ir de fato cumpri-la. +Alm de brincar, uma experincia. +Pela TV voc v. +A diferena que no videogame voc sente. + como se estivesse l, acrescenta Rodrigo. +H outros apelos da fita que seduzem os gamemanacos, especialmente os fs de Ayrton Senna. +No incio de cada corrida, por exemplo, as principais dicas do circuito so fornecidas pelo piloto. +A imagem de Ayrton aparece em fotos digitalizadas. +Um dos responsveis pela seleo das moas da Ford, que no quis se identificar, aponta o italiano Maldini (considerado pela imprensa europia o bumbum mais bonito da Copa), que recebeu apenas um voto, como o mais bonito. +Este sim, seria modelo com certeza, disse. +Os reservas da seleo brasileira Viola e Ronaldo e vrios componentes da Nigria tambm foram apontados. +O Viola ou o Ronaldo seria um 'black man' perfeito disse um dos modelos masculinos da Ford, que tambm no quis se identificar. +Esteve assim com o astro da conferncia, lorde Keynes um sujeito sutil, bom expositor, muito persuasivo, conta Campos. +A delegao britnica era a mais forte em termos intelectuais, embora outros participantes tivessem dela uma opinio algo debochada. +Corria uma piada segundo a qual Keynes era inteligente demais para ser consistente; Dennis Robertson, outra estrela, era consistente demais para ser inteligente; e Lionel Robbins, o terceiro nome de prestgio, no era nem inteligente nem consistente. +Durante a negociao, ontem tarde, Muller telefonou para o presidente do So Paulo, Fernando Casal de Rey, e se lamentou. +Ele reclamou de que no lhe pagariam o prometido, no lhe dariam um carro e que s bancariam seis meses de aluguel de sua casa e no os quatro anos, como esperava, disse o dirigente. +Ela preferiu ser a primeira dama da ilha de Skorpios. +Foi nesse papel que, encarnada por outra Jacqueline (Bisset), Jackie O. debutou na tela, 15 anos depois de ter-se transformado na mais pranteada e sedutora viva deste sculo. +Merecia coisa melhor. +Alm de muito ruim, o filme O Magnata Grego (The Greek Tycoon) dedicava mais ateno ao seu segundo marido, interpretado por Anthony Quinn, que alis faria o papel de Scrates Onassis (sogro de Jackie) no telefilme Onassis: The Richest Man in the World, produzido em 1988. +O presidente da Portuguesa, Manuel Pacheco, deve entrar tera-feira com uma ao contra o Vasco da Gama. +Ontem tarde, Pacheco disse que esperar at tera-feira para que o clube carioca envie uma cpia do seguro de vida previsto no contrato de emprstimo do meia Dener, morto tera-feira. +Mas, no Rio, o diretor de futebol do Vasco, Eurico Miranda, confirmou que o seguro no foi feito. +Segundo Miranda, a legislao brasileira probe que um seguro desta natureza tenha como beneficirio uma empresa ou associao. +Resultado: presso nos preos combinada com a falta de produtos. +Se a queda da inflao certamente produziria efeitos positivos na candidatura Fernando Henrique Cardoso, as prateleiras vazias municiam os adversrios. + certo que, desta vez, o governo est mais atento do que no Plano Cruzado e vai usar, segundo o ministro Rubens Ricupero, todos os artifcios para evitar uma febre de consumo. +Se vai conseguir, outro problema. +Mais uma vez, se v que a deseducao o problema que mais sai caro ao Brasil. +PS Realmente notvel a evoluo de Lula nos ltimos 15 anos. +Recebi ontem por fax entrevista que ele concedeu em 1979 para a revista Especial, onde expe sua viso sobre a mulher. +A gente no pode pensar em jogar a mulher no mercado de trabalho enquanto houver excesso de mo-de-obra. +Alis, nessa entrevista ele, corretamente, defende a legalizao do aborto com argumentos razoveis e, agora, por motivos eleitoreiros, cedeu presso da Igreja Catlica. +A campanha eleitoral encerrou-se na noite de quinta-feira, com um pronunciamento de oito minutos de todos os candidatos. +A propaganda eleitoral nas redes de TV custou ao uruguaio mais de US$ 14 milhes (cerca de US$ 7,00 por cada eleitor). +Pela lei, os partidos recebem recursos do governo de acordo com os resultados da ltima eleio. +Acho importante notar que a Bienal mudou a maneira de conseguir patrocnio cultural no pas, que antes era feito base do me d um dinheiro a. +Vendendo cada sala especial para um patrocinador exclusivo, demos a ele um retorno muito mais palpvel, com anncios em meios de comunicao e o abatimento de impostos. +Tratamos a Bienal como empresa. +Folha -- O nmero de visitantes pagantes da Bienal do dia da abertura (12 de outubro) at o dia 7 deste ms foi de 103 mil. +Se a Bienal for at o dia 11 de dezembro, dever ter um pblico total de no mximo 250 mil pagantes, alm de quase 200 mil crianas, seguindo a projeo. +O Sr. acha um bom resultado? +Para o volante Dinho, um dos jogadores responsveis pelo setor de marcao do Santos, impossvel segurar o ataque do So Paulo do tcnico Tel Santana. +A sada, diz, partir para o ataque. +Sobre sua sada do So Paulo no incio do Campeonato Paulista o jogador diz que a questo assunto encerrado. +Depois da Portuguesa, esta tarde no Canind, o Palmeiras far trs jogos seguidos em seu estdio: contra o Bragantino, tera-feira, a Ferroviria, quinta-feira, e o Amrica, domingo. +Se vencermos esses quatro jogos, chegaremos a 80% de aproveitamento dos pontos disputados. +Acredito que, mantendo esse percentual, seremos campees, disse Luxemburgo. +Gianetti da Fonseca afirmou que a avaliao tem que ser feita por pares (pesquisadores da mesma rea), que no faam parte do grupo analisado. +Freddy Tatoo vive atualmente em Bolonha e j est acostumado ao circuito internacional da tatuagem. +Ele j tatuou, pelo menos, mil pessoas. +Pelo grupo brazuca, ainda estaro presentes Tyes e Caio (Rio de Janeiro) e o especialista em body piercing, Andr Meyer (So Paulo). +Eles vo ser indiciados por suposta corrupo de menores, favorecimento prostituio e crcere privado. +O advogado Jos Carlo Dri, que defende os responsveis pela boate, se recusou a falar. +WILSON BALDINI JR. +O norte-americano Evander Holyfield coloca o ttulo mundial dos pesos-pesados (acima de 86,183 quilos), verso Associao Mundial (AMB) e Federao Internacional (FIB), hoje, contra o seu compatriota Michael Moorer. +Quatro vereadores na cidade e o vice-prefeito afirmaram que votaro em Fernando Henrique Cardoso, embora seus partidos (PMDB e PPR) tenham candidatos prprios a presidente. +Voto no Fernando Henrique porque ele fez o Plano Real, afirmou o vice-prefeito, Manoel Lopes Duarte, do PPR. +A coreografia criada por Bil T. Jones para An American Evening chama-se I Want to Cross Over, com msica gospel cantada por Liz McComb. +O cenrio, de Donald Baechler, se compe de pedaos de um barco, uma pequena casa e um tubo de chamin. +Negro e portador do vrus da Aids, Bill T. Jones uma das expresses mais poderosas da dana contempornea. +Agora, alm de dirigir seu grupo sediado em Nova York, ele tambm vai atuar como coregrafo-residente do Ballet da pera de Lyon, que acaba de nome-lo para o cargo, antes ocupado por Maguy Marin. +Ex-bailarino do grupo de Trisha Brown, Stephen Petronio outra estrela do momento. +Para An American Evening ele criou Extra Veinous, cujo ttulo significa o contrrio de intravenoso. +A coreografia de Susan Marshall que o Ballet de Lyon dana hoje chama-se Central Figure, com msica de Philip Glass. +Inspira-se no mais antigo e melhor bailarino de minha companhia, que morreu no ltimo vero, ela diz. +O tema de Julia Child era uma simples quiche, mas era poltico: mostrar aos paranicos americanos que o ovo e a manteiga fazem pratos deliciosos e que a patrulha do colesterol deve dar espao ao prazer da comida. +Marcella Hazan falou sobre alcachofras e catequizou o pblico sobre a supremacia do leo de oliva extra virgem para cozinhar, apesar da exorbitncia do preo. +Patricia Wells ensinou o preparo de codornas marinadas e cuscuz marroquino e incentivava o pblico a repartir food experiences (o nome moderno para receber amigos para comer) como ela mesma faz em sua casa na Provence (sul da Frana), regio em moda nos Estados Unidos. +Os irmos Gershwin eram to diferentes que se completavam s maravilhas. +George fazia a msica, Ira a letra, embora, como em toda parceria, cada qual desse palpites na especialidade do outro. +Primeiro, George fazia a msica; Ira capturava o esprito da cano e apunha-lhe ttulo e letra. +s vezes sugeria a George uma mudana no andamento, para que este se adequasse melhor letra. +Um caso tpico foi o de Someone to Watch Over Me (1926), que nasceu brejeira e Ira recomendou que ela se tornasse uma balada romntica. +George era um dnamo, bomio e namorador; Ira era mais recluso e solidamente casado. +George era indiferente politicamente; Ira era liberal, com inclinaes socialistas. +A versatilidade do George como compositor s era igualada pela de Ira como letrista. +Os dois eram bambas tanto nas canes mais rtmicas, alegres e humorsticas, como nas mais srias, romnticas e profundas. +Segundo a administrao do aeroclube, a quadrilha conseguiu decolar s 4h, sob forte nevoeiro e sem iluminao. +O avio do mdico Domingues Braille, dono de uma clnica em So Jos do Rio Preto. +O jogador, sabendo que ganharia mais de US$ 250 mil, fez o que a voz ordenou. +A roleta girou, e ele perdeu. +Droga, disse a voz em sua cabea. +Dividido em seis partes, o livro traz 32 captulos que procuram abranger todos os recursos do programa. + bastante didtico e utiliza bem desenhos para exemplificar efeitos especiais. +O texto curto e prtico e todos os captulos so recheados de notas, dicas e alertas, ilustradas com as respectivas figuras Note, Tip e Stop. +Com direo de Roberto de Oliveira, a Bandeirantes prepara um documentrio sobre a Campanha Contra a Fome e o trabalho do rapaz. +O especial dever ir ao ar no Brasil e em vrios pases poucos dias antes da eleio para o Prmio Nobel da Paz. +O mais grave, no entanto, so os problemas que os clientes podero enfrentar. +A quem recorrer no caso de um descomprimento do que prometido? +Notamos ainda que, quando um passageiro brasileiro se inscreve num programa de milhagem de uma companhia area internacional que serve o Brasil, passados 30 dias, recebe a proposta da Iapa. +Ou seja, se tal procedimento no for ocasional e tiver realmente a conivncia das transportadoras areas, estas esto expressamente contribuindo para a concorrncia desleal e ilcita perante os agentes de viagens alm de no preservar seus clientes, que no so consultados sobre se querem ou no receber tais propostas ou se autorizam a divulgao de seus nomes e endereos. +Como partiu do PT o pedido de encontro com o presidente Nelson Mandela, o prprio partido assumiu os custos do trecho da viagem que inclui a frica do Sul. +Segundo a tesoureira da campanha de Lula, Tatau Godinho, os custos se resumem passagem area, j que o candidato ficou hospedado na casa do embaixador do Brasil na frica do Sul, Antnio Amaral de Sampaio. +Ela no tem os valores exatos pagos pelo partido. +MAIS RETIRADOS: O nmero da esquerda representa a posio atual e o da direita, a posio na quinzena anterior. +Consulta realizada nos dias 28 e 29/03/94 junto s locadoras Cine Arte Vdeo, Gentile Vdeo Hobby Vdeo, Over Vdeo, Real Vdeo, Vdeo Clube do Brasil, Vdeo Factory e Wolf Vdeo. +RECOMENDADOS: avaliao feita a partir de fitas emprestadas por distribuidoras de todo o pas. +Derrida -- A palavra engajado tem uma histria. +Quando a gente se diz engajado, corre o risco de evocar modelos anteriores e o engajamento hoje deve encontrar formas novas. +Mas o trabalho no Parlamento uma forma de engajamento, claro. +Folha -- Obrigada pela entrevista. +A notcia veiculada por esta Folha (em 10/10/1994) de que o presidente eleito Fernando Henrique Cardoso pretende propor o fim da unicidade sindical (sindicato nico representando a mesma categoria e na mesma base territorial, conforme est previsto no artigo 8, II da Constituio Federal de 1988) juntamente com o contrato coletivo de trabalho, demonstra sua coerncia com a modernizao das relaes sociais. +Com efeito, os estudiosos da matria afirmam que a negociao coletiva de trabalho est diretamente relacionada com o modelo de organizao sindical (cf. Amauri Mascaro Nascimento, em Direito Sindical, editora Saraiva, 1989, pg. 313). +No houve mortos. +Cerca de 30 feridos leves foram atendidos na Santa Casa da cidade. +O prefeito Mercedes Ribeiro de Miranda (PMDB) decretou estado de calamidade pblica na cidade, que ficou sem gua e luz durante 22 horas. +Todos os anunciantes que aderiram idia dos clubes infantis esto atrs de um mercado de dimenses gigantescas: os miniconsumidores americanos na faixa de idade entre 4 e 12 anos gastaram no ano passado US$ 7,3 bilhes das suas prprias mesadas e influenciaram suas famlias a comprar mais US$ 130 bilhes. +Tanto dinheiro parece tornar desprezvel a tica do marketing: a Delta recebe centenas de cartas de crianas denunciando pais ou parentes que preferiram viajar por outras companhias areas, configurando um claro estmulo ao dedodurismo precoce. +Helcio Emerich jornalista, publicitrio e vice-presidente da agncia Almap/BBDO. +Fabiane recebeu CR$ 35 mil para usar camiseta da candidata e gritar o nome de Qurcia. +Deus me livre. +No voto em Qurcia de jeito nenhum. +Estou aqui a trabalho, disse. +Ela pertncia a um grupo de cem garotas de Braslia contratado para fazer propaganda de Ana Paula Junqueira. +Acho absurdo um poltico pagar algum para gritar seu nome, reclama. +O tcnico irritou-se com uma pergunta e no a respondeu. +O Brasil nunca esteve em apuros, segundo o tcnico. +A frente do glaciar, que fica na beira do rio, desprende blocos de gelo do tamanho de um prdio de 30 andares. +D para sentir a terra estremecer e o barulho parece o de um trovo. +Fomos convidados a fazer um minitreking, ou seja, caminhar claando crampones (sapatos com pregos de ferro) sobre o glaciar. +Durante o passeio, escutvamos os rudos da movimentao lenta deste enorme rio de gelo e contemplvamos os contrastes cromticos da superfcie congelada refletindo o cu azul. + preciso ter em conta que a elevao das importaes com a reduo das alquotas estimada em 1% da pauta das importaes brasileira. +Esse percentual razoavelmente pequeno porque significa US$ 250 milhes, disse. +Amaral disse ainda que esse aumento de importaes decorre do aumento da demanda que se exerce sobre toda a oferta de produtos. +ENFERMAGEM -- Acontecer no prximo dia 6, no Ces-Senac (av. Tiradentes, 822), seminrio sobre a caracterizao do pessoal de enfermagem no Estado de So Paulo. +A presena deve ser confirmada at o prximo dia 5, pelo tel. 221-2155. +OFICINA DE LIVROS -- A Oficina de Artes do Livro oferece 8 vagas para o curso Papel Artesanal: Processamento de Fibras Vegetais, que vai de 5 a 14 de abril. +Informaes pelo tel. 212-2051. +O documento tambm sugere a criao de uma sistemtica mensal de acompanhamento. +Elvis morreu quando entrou para o exrcito. +A caixa com cinco CDs Elvis: From Nashville To Memphis enterra em parte o velho mito explicitado pelo beatle. +O pacote traz as gravaes essenciais do cantor na dcada de 60, exatamente depois de sua baixa no exrcito norte-americano, em 6 de maro de 1960. +Esses so os anos em que Elvis deixou de ser o rei do rock'n'roll para tornar-se o smbolo pr-fabricado de uma gerao que sonhava com Honolulu e pracinhas. +Nessa poca, o cantor forjou alguns de seus maiores sucessos como, para ficar num s exemplo, It's Now or Never. +Tambm nesse perodo Elvis Aaron Presley sucumbiu ao monstro chamado Elvis Presley. +O cantor no esteve livre do assdio e do culto de seus fs nem mesmo durante os dois anos em que esteve ligado ao exrcito. +E justamente por isso que no se deve parar, preparando terreno ao futuro presidente. +Alm de aumentar o nmero de cassaes, deve-se estimular as demais CPIs sobre Empreiteiras, financiamentos eleitorais e CUT a CPI da CUT ter o dom de trazer tona possveis desvios da burocracia e da praga do corporativismo, o que, acreditem, vai acabar ajudando no apenas o PT mas a democracia. +Mas a roubalheira apenas a ponta do lamaal: o problema essencial, a grande delinquncia, a baixa taxa de seriedade e compromisso dos homens pblicos, responsvel por estarmos atolados em nveis jamais vistos de misria e violncia e, como diz o salmo, a que no se pode estar em p. +PS Milagre mesmo gente como Joo Alves trocar a leitura de volantes de loteria pela Bblia quem sabe o poder pblico no o ajuda, dando-lhe uma cela especial para aprofundar tais leituras. +Vocs so pessoas de um Brasil que tem sinais da renovao. +Se Roseana, os senadores, deputados e prefeitos esto me apoiando no porque tenhamos firmado qualquer compromisso pessoal. +Jamais ningum me pediu nada. +Mas, nada mesmo, disse FHC. +s 16h, antes de embarcar para a cidade de Cod (MA), onde seria realizado um comcio s 18h de ontem, Fernando Henrique afirmou que vai aguardar o posicionamento do senador Jos Sarney (PMDB-AP) em relao s eleies presidenciais. +Folha -- Como voc recebeu a notcia de que seria substitudo? +Giovane -- Normalmente. +Seleo isso, joga quem est melhor, no tem esse negcio de nome. +A melhor equipe que deve jogar. +... casos positivos de dengue em Monte Aprazvel (38 km de Rio Preto-SP) foram divulgados ontem pelo Ersa (Escritrio Regional de Sade) de Rio Preto. +A regio vive uma epidemia da doena. +A cidade de Monte Aprazvel tem 68 casos confirmados. +Rio Preto tem trs casos positivos. +Os policiais federais de Mato Grosso do Sul entraram em greve ontem, em adeso ao movimento iniciado no Distrito Federal. +A reivindicao de equiparao salarial com a Polcia Civil do DF, o que representaria um reajuste de 300%. +Em Mato Grosso, a paralisao est prevista para comear hoje. +Todas receberam beijos de Itamar. +Fu foi a mais calorosa. +Beijou, abraou duas vezes e conversou por alguns minutos com o conterrneo presidente. +Eu torci muito por voc, viu?, disse Itamar a Fu. +Cada usurio pode importar programas at o valor de US$ 200 mil. +Os pedidos at US$ 20 mil dispensam guia de importao. +Se o valor da compra for baixo, melhor pedir encomenda por correio normal. +Pelo correio expresso o frete custa prximo de US$ 50. +Se no quiser se aventurar pela importao direta, existem empresas, como a Brasoftware (tel. 011/253-1588) que importam programas sob encomenda. +Segundo Ricardo Jordo, gerente de marketing da Brasoftware, o produto -- entregue no mximo em 15 dias -- sai por um preo similar importao feita pelo usurio. +A taxa de servio da Brasoftware coberta pelo desconto que obtm junto ao fornecedor. +ARRIET CHANIN -- A artista mostra 15 monotipias trabalhadas em papel artesanal de folha de bananeira e 12 gravuras em metal, que so instrumentos musicais e conchas. +De seg a sex das 11h s 19h e sb das 10h s 13h. +Preos das obras: de R$ 50 a R$ 500. +At 30 de setembro. +As novas impressoras a laser da HP vm com um novo padro de velocidade 12 pginas por minuto (ppm) e so de 30% a 40% mais rpidas que as da gerao anterior. +As LaserJet 4 Plus e 4M Plus substituem os modelos 4 e 4M. +Tm resoluo de 600 x 600 pontos por polegada (dpi), o que permite obter imagens com mais definio. +O empresrio e corretor de imveis, Roberto Capuano, foi reeleito pela terceira vez presidente do Conselho Regional de Corretores de Imveis do Estado de So Paulo (Creci). +Ser a sua quarta gesto frente do rgo. +Enquanto Gloria Pires ficou nervosa em sua estria como modette, o marido Orlando Moraes amou e pediu bis. +De acordo com a Enciclopdia Britnica, o linchamento uma prtica que se verifica em momentos de instabilidade ou de ameaa de anarquia. +A sensao de insegurana e a debilidade do poder pblico eram sem dvida caractersticas dos Estados Unidos do final do sculo 18, quando (segundo a verso mais aceita) a palavra teria sido inventada a partir da prtica adotada por um juiz Lynch, de condenar e executar seus adversrios sem o devido processo legal. +Lamentavelmente so tambm caractersticas de diversas regies do Brasil de hoje. +O brbaro linchamento de trs pessoas ocorrido esta semana no Paran pode ter sido particularmente chocante e sem dvida o foi, filmado e transmitido pela televiso em toda a sua brutalidade, mas constitui apenas mais um na lista dos crimes desse tipo registrados no pas. +Apenas na Bahia, por exemplo, o comando da Polcia Militar estadual informa que foram 350 casos nos ltimos quatro anos. +Formada por tcnicos da Fazenda e das Minas e Energia, a comisso tem prazo de 60 dias para conluir o trabalho. +O objetivo e melhorar a rentabilidade do setor. +Segundo o ministro das Minas e Energia, Alexis Stepanenko, essa melhoria dever ser buscada atravs da reduo de custos e aumento de eficincia. +No dia sete de dezembro de 1941, avies japoneses praticamente destruram a frota norte-americana no Pacfico, ancorada em Pearl Harbour, no Hava. +Os EUA, depois de um histrico discurso de seu presidente, Franklin Delano Roosevelt, entraram em guerra contra o Japo e seus aliados no Eixo, Alemanha e Itlia. +O chamado mundo livre (que ento inclua tambm a URSS) unia-se contra o nazi-fascismo. +E o Brasil? +No Departamento de Estado, em Washington, desconfiava-se de Getlio, ditador com uma poltica em certos momentos prxima ao fascismo mussoliniano. +Vargas, na verdade, preferia ficar de fora, ou aliar-se com os vencedores. +A Argentina, por exemplo, era bem mais pr-nazi do que o Brasil. +Welles foi mandado como uma espcie de embaixador cultural. +No foi o nico. +E ele gostava de Roosevelt e detestava os nazistas. +J estou trabalhando com as principais figuras da vida poltica do pas com senadores e secretrios de governo. +Agora estou trabalhando no desenvolvimento de um programa para aumentar a contribuio das polticas pblicas para o capital social. +Mas so necessrias estratgias da base da sociedade. +A substncia tambm usada em vrios medicamentos vendidos no Brasil, que dizem ser capazes de aliviar dores causadas por varizes e eliminar cimbras noturnas em pessoas que sofrem problemas de circulao nas pernas. +Levantamento realizado entre pacientes que ingeriram cpsulas com a substncia entre 1969 e 1992 indicou que 16 pessoas podem ter morrido em consequncia de contra-indicaes causadas pelo sulfato de quinino. +Conforme acordo autorizado pelo BC, o saldo da dvida do Estado com a Nossa Caixa de US$ 1 bilho ter que ser pago este ano, em doze parcelas mensais, atualizadas e com encargos. +Corresponde a duas vezes e meia o patrimonio liquido da instituio. +O Banespa carrega US$ 8,1 bilhes de crditos do setor pblico. +Segundo o detetive Paulo Arajo, a delegacia recebeu um telefonema de homens que se identificaram como traficantes do morro Azul (no Flamengo). +Segundo o detetive, eles ameaaram invadir a delegacia, no horrio de visita, para resgatar dez companheiros. +Os fundos de commodities projetam para este ms, na mdia, rentabilidade bruta de 3,72%, segundo a Anbid. +A rentabilidade lquida, para saque em 1 de setembro, vai depender da variao da Ufir. +Por enquanto, este indexador projeta variao de 1%, com o que os fundos de commodities renderiam 3,03%. +Os norte-americanos que se reuniram ontem na Cmara de Comrcio Brasil-Estados Unidos, em So Paulo, ficaram conformados com a derrota de sua seleo. +J estamos contentes por ter chegado s oitavas-de-final, disse Phillip Trent, 24, que assistiu o jogo ao lado do amigo Brian Fagerburg, 24. +Ambos seguraram uma bandeira dos Estados Unidos durante a partida. +O desembargador Doreste Batista, convidado pelo governador do Rio, Nilo Batista, para ser o supersecretrio de Segurana do Estado, disse ontem que, se confirmado, vai pedir apoio do Exrcito para subir morros e colocar policiamento ostensivo nas ruas. +At o meio-dia de ontem, o desembargador estava tentando um contato com o governador para definir se aceitava ou no o cargo. +Ao lado do ala Charles Barkley, Shaq hoje um dos maiores astros do basquete mundial. + piv do Orlando Magic, equipe da NBA, liga profissional de basquete dos EUA. +Conquistou o ttulo mundial deste ano com a seleo de seu pas, o Dream Team (Time dos Sonhos) 2. +Enfermeiro uma designao vlida apenas para quem concluiu o curso superior de enfermagem. +Os outros profissionais da rea so tcnicos, auxiliares e atendentes. +O tcnico tem o segundo grau completo pode optar entre o curso tcnico de enfermagem ou curso de formao de tcnico. +Os limites da privatizao so as necessidades do governo. +Nada est excludo, afirmou Montoro Filho. +Para ele, a privatizao entende desde a venda de ativos pblicos at aes de estatais. +Disse que o governo vai vender desde automvel at participao acionria em empresas de energia eltrica. +O dinheiro das vendas pode ser utilizado tambm para o pagamento da dvida do Banespa. +E tudo, segundo a Jovem Pan, para fazer proselitismo, para as promessas de sempre. +Fernando Henrique apareceu no rdio e na televiso dizendo que no existe nada contra o seu vice. +E que, portanto, nem pensa numa troca de Guilherme Palmeira. +Uma defesa bem parecida com aquela de Lula, no comeo das denncias contra Jos Paulo Bisol. +No tnhamos uma segurana exemplar, mas ela era suficiente para permitir o alvar de licena dos bombeiros, afirmou. +Segundo ele, o alvar foi queimado, com tudo o que se perdeu. +Taranto no soube dizer se os parentes das vtimas podero pedir alguma indenizao. +A MTV no exibiu ontem o desenho animado Beavis e Butt-Head por determinao de liminar expedida pela Vara Central da Infncia e Juventude, em So Paulo. +Entre 20h e 20h30, horrio em que o cartoon costuma ir ao ar, a emissora transmitiu uma sequncia de videoclipes. +Antes, informou sobre a proibio da Justia num comunicado de 15 segundos. +Os nativos ainda se utilizam de lanas primitivas para pescar nas rasas piscinas naturais repletas de polvos e peixes tropicais. +Mas h a pesca comercial de atum controlada por norte-americanos de origem portuguesa, chineses de Taiwan e sul-coreanos em embarcaes de 1,2 tonelada que valem at US$ 13 milhes. +Patologistas de hospital de Riverside (Califrnia) tiveram que vestir roupas especiais para autopsiar corpo de uma mulher que teria qumicos txicos no sangue, porque o cheiro os fez desmaiar. +Cristou representa os clubes alemes do Bayern Leverkusen e Shalk-04. +O empresrio disse que pretende ver tambm outros jogadores brasileiros, entre eles Cafu, do So Paulo, Mirandinha, do Paysandu, e Claudinho, da Ponte Preta. +O caso nasceu de uma vendetta, obra e desgraa de um certo Francesco Farina, dono do Modena, um time rebaixado 3 diviso. +Farina tentou salvar o Modena na poltica e no tapeto. +Obviamente, no conseguiu. +Por isso, metralhou Antonio Matarrese, presidente da federao. +Farina acusou os clubes de sonegarem impostos com a cumplicidade da federao. +Para multiplicar o porte das suas denncias, ele apresentou as suas peroraes em dois stios diferentes, s procuradorias de Roma e de Milo. +Muitas de elas so inconsequentes, improvveis, sem documentao. +De todo modo, na Bota, Justia cabe a misso de investigar, antes de meramente julgar. +Gloria Attanasio, magistrada da capital, meramente decidiu o que a sua funo lhe exigia, e enviou 250 detetives s sedes dos tais 34 clubes para, l, coletarem os livros contbeis e os recibos correspondentes ao versamento dos impostos. +Para a construo do palco foram utilizadas 170 toneladas de ferro e alumnio. +A quantidade de ferro daria para construir 180 carros iguais a um Santana. +E o volume de alumnio daria para fazer 275 mil latas de refrigerante. +O palco foi criado por Mark Fischer, responsvel pela montagem do palco da ltima turn dos Stones Steel Wheels, do novo show do Pink Floyd e do espetculo ZooTV do grupo irlands U2. +Apesar da popularidade, Chico de Miguel perdeu as duas ltimas eleies para a Prefeitura de Itabaiana, que passou a ser controlada por polticos locais ligados ao governador Joo Alves. +Ele atribui as derrotas ao Judicirio, acusado de auxiliar seus opositores. +No explica como, mas garante que no vai perder a prxima eleio em Itabaiana. +O deputado afirma no ser um homem rico. +Hoje, tenho s uma fazenda e pouco mais de 500 cabeas de gado. +Mas tudo que ganho reverto tambm em ajuda para o povo, que recebe remdios, feira e dinheiro quando est precisando. +Nunca fui egosta, afirma. +Outros institutos mostram nmeros diferentes, mas no so divulgados pela televiso. +A Liga de Assistncia e Recuperao, rgo ligado Prefeitura de Salvador, est desenvolvendo um projeto para a confeco de brinquedos a partir de sucatas. +Crianas de 11 a 14 anos vo aprender a transformar sucatas em brinquedos educativos. +A Secretaria de Sade da Paraba informou que Joo Pessoa est entre as cinco capitais brasileiras que atingiram 90% de cobertura vacinal na ltima campanha de multivacinao, realizada em 90. +Neste ano, a secretaria espera vacinar cerca de 480 mil crianas. +A Rssia vetou documento da Conferncia sobre Segurana e Cooperao na Europa sobre a ex-repblica iugoslava da Bsnia. +O veto impediu a declarao que pedia aos srvios da Bsnia que parassem ataques no encrave muulmano de Bihac. +A reunio terminou ontem em Budapeste. +Se o preo do nibus for convertido pela tabela da montadora, o impacto ser de 10% a 12% na tarifa, diz Dias. +As tabelas das montadoras, segundo ele, esto 40% acima do preo de mercado. +O Frum j tem os preos mximos pagos no mercado. +nibus leve, US$ 55 mil, para o pesado, US$ 70 mil. +Antes do Mundial, a delegao brasileira participa dos Jogos da Amizade que tm incio dia 23, na Rssia. +William Gibson um escritor normalmente associado fico cientfica. +De fato, a maioria de suas histrias acontece num tempo algo frente do presente, mas Gibson rompe completamente com a idia fundadora da FC no cinema ou na literatura de futuro como evoluo. +A estratgia de Gibson consiste em projetar no futuro o que j est latente no mundo contemporneo. +Nos romances e contos do escritor, o cenrio muito prximo do atual, s que visto com uma certa distoro alucingena, um certo exagero de imaginao. +Valores mdios das locaes comerciais aumentam 50,16%. +Os valores mdios das locaes de imveis no residenciais na cidade de So Paulo subiram em junho 50,16%, segundo a Hubert Imveis e Administrao Ltda. +A evoluo foi menos acelerada que a verificada em maio (55,16%). +A maior desacelerao ocorreu na regio da Avenida Faria Lima. +A Polcia Federal prendeu ontem em Braslia Vicente Wilson Rivera Ramos, o Vicentico, filho de um dos chefes do Cartel de Cli (organizao colombiana que trafica drogas). +Vicentico havia escapado do cerco da PF, no domingo, quando foram apreendidas 7,5 toneladas de cocana em Guara (TO). +Filho de Vicente Rivera Gonzalez, ele j foi condenado na Holanda a 14 anos de priso, segundo a PF. +Os receios de Durante quanto reao da platia eram compartilhados pelo restante da comitiva. +Documentos reservados da segurana da Presidncia da Repblica alertavam, na semana anterior, para a possibilidade do presidente ouvir vaias. +Mais que isso: os papis da segurana afirmavam que, em razo da inflao alta, no se deveria descartar sequer o risco do pblico atirar objetos no camarote presidencial. +Em telefonema a Maurcio Corra, ministro da Justia, o governador do Rio, Leonel Brizola, desaconselhou a ida de Itamar ao Sambdromo. +A reao do pblico imprevisvel, disse. +Avisado do alerta do governador, Itamar desdenhou os riscos e manteve a viagem. +Corra trocaria a preocupao pela descontrao. +Agarrado ao copo de usque, foi outro destaque da noite. +A Kodak ainda passou a atender encomendas de peas plsticas para a Philips, Wapsa e NGK, entre 16 clientes. +Uma nova diviso de servios, batizada de Kis, opera na administrao de copiadoras e outros equipamentos de imagem para grandes empresas. + um nicho promissor. +Segundo Galan, os novos produtos e servios acrescentaro 10% ao faturamento, que foi de US$ 360 milhes em 93. +No prximo ano sero mais 15%, prev. +White -- Acho que sim. +A prpria psicanlise est baseada numa concepo tropolgica da conscincia humana. +A Interpretao dos Sonhos, um dos textos fundadores da psicanlise, apresenta toda uma tropologia do ato de sonhar e d a base para uma potica que combina a teoria da tropologia com a noo da inveno potica. +A idia de tropos provisria. +Uma das coisas que os linguistas ainda precisam estudar a teoria dos tropos. +Roman Jakobson trabalhou nessa direo. +Lacan desenvolveu os conceitos de metfora e metonmia para caracterizar certos modos de conscincia. +No se pense que esta ruptura modernizante passa pelo PT. +O partido um aglomerado heterogneo, que s se mantm unido pela perspectiva de vitria de Lula. +Teses modernas e sociais como o controle civil sobre o sistema de sade esbarram em resistncias enormes dos setores hegemnicos do PT, sob o argumento de que amarrariam a atuao do governo, logo agora que o partido se prepara para assumir o poder. +Tampouco passa por FHC, representante da contemporizao bem explicada, ou por Antonio Britto cujo voto, em favor da anistia total aos agricultores, semana passada, ressuscitou os piores prognsticos sobre seu estilo. +O motorista teria perdido o controle do carro, batido em um Monza e em uma rvore e capotado na rua Enas Luiz Carlos Barbante. +Maria Elisa Flora Demarchi, 15, est internada em estado grave no Hospital do Mandaqui. +O boneco servir para criar o que chamamos de 'efeito aurola' no motorista. +Pesquisas comprovam que quando o motorista v a fiscalizao ele reduz a velocidade por pelo menos trs quilmetros, diz Lehfeld. +O boneco vai ser colocado em locais sem congestionamento. +Vai trabalhar das 7h s 17h30, inclusive nos finais de semana. +A permanncia em cada cabine ainda no foi definida. +Perry descreveu trs fases para a operao: a entrada no Haiti e a conquista de todas as posies estratgicas (com durao de seis a horas a dois dias), a criao de um ambiente seguro (de trs a seis meses) e a transferncia do controle da situao para a ONU. +O presidente Bill Clinton passou a maior parte do dia de ontem em reunies com seus assessores militares e de segurana nacional na Casa Branca. +Vila Madalena e Pinheiros ganharam novas opes noturnas nesta semana. +O bar e restaurante Lanterna inaugurou na tera-feira um espao para danar. +Agora o jardim entre a galeria e o salo do restaurante propriamente dito tem um bar com som dos DJs Bidi, Bartolo e Hori. +O trnsito nas avenidas Paulista e Brigadeiro Lus Antnio (zona central de So Paulo) parou no dia 9 de setembro de 86. +O aposentado Eliseu Francisco de Lyra (na poca com 44 anos), depois de discutir com o irmo, saiu do carro, subiu no cap, tirou as roupas e sapateou. +A AI, organismo internacional com mais de 1,1 milho de membros, divulgou o documento ontem em So Paulo. +O relatrio, referente ao ano de 1993, tambm destaca a impunidade dos acusados pelos massacres do Carandiru (outubro de 92), da Candelria (julho de 93) e de Vigrio Geral (agosto de 93). +O apelo pop do evento j est causando reaes. +Ohtake diz que at o compositor e cantor baiano Caetano Veloso manifestou desejo de assistir ao show de Nusrat, nesta segunda-feira, em So Paulo. +Um monte de artistas vai querer ver os concertos, diz Ohtake. +O secretrio espera a resposta do pblico. +Toda platia de msica erudita tradicionalista, explica. +Em Campos do Jordo tambm. +O pblico de l extremamente treinado em msica do sculo 19. +Para mim muito estimulante oferecer uma idia espacial para um trabalho que comeou a partir de uma idia musical. +Basicamente, o que tenho feito com os msicos com os quais trabalho. +O que mudou talvez seja meu vocabulrio, que est menos estruturado. +A dinmica, a maneira como os movimentos so construdos, hoje diferente. +1. Currculo com mais de trs pginas. +2. Que o selecionador tenha que ler todo o currculo para descobrir o perfil do profissional. +O crtico (/deve ser), um insolente da razo? +Um (mecenas) nobre que deifica o saber antes e mistifica o dar depois ou ele deve ser um iconoclasta, espalhar o que sabe enquanto o tempo faz das novidades fatos descartveis. +Ser que j no nos basta a humilde compulso passiva dos leitores que aceitam nos ler para serem informados? +Mas o leitor tambm sabe se vingar com o desdm e a indiferena com que capaz de folhear as pginas da sua busca. +Um crtico de jornal cotidiano deve se conformar com o papel de narrador e seu destaque secundrio ao simples gesto de uma pgina ser virada e ficar para trs. +A notcia um fato, no um exerccio acadmico. +A dimenso do fato est na relatividade da sua importncia para cada leitor e no no espao narcsico que ocupa sob uma assinatura. +Em Medicina, ganharam Richard Dart e Richard Gustafson, da Universidade de Arizona por fracassarem ao tratar um paciente mordido por uma cascavel com choques eltricos aplicados no lbio durante cinco minutos. +Em Fsica, ganhou a Agncia Meteorolgica Japonesa por um estudo de sete anos sobre a hiptese de terremotos serem causados por peixes rebolando suas caudas. +Na madrugada de ontem, os prdios foram vigiados a distncia por homens da Rota, que estavam sem farda. +Segundo o coronel, eles viram o armamento da quadrilha ser retirado de um dos prdios em um Gol. +Esse carro foi achado no incio da tarde em Engenheiro Marsilac (zona sul). +Com mandados de busca e apreenso, os homens da Rota invadiram os trs apartamentos. +No houve tiroteio. +De acordo com o coronel, a polcia descobriu a quadrilha atravs de uma denncia annima. +Eduardo Loureno -- No se deve esperar uma interveno que tenha efeitos imediatos como a dos polticos. +Os escritores aqui reunidos pretendem alertar a comunidade internacional sobre os ataques sofridos pela liberdade de pensar e de escrever em vrios pases do mundo. +Os exemplos mais clebres e trgicos so os de Rushdie e Nasreen. +O nosso protesto de ordem moral, temos a obrigao de defender uma das grandes tradies da nossa civilizao, que a da liberdade de expresso. +Jos Saramago -- A contribuio vai depender do eco que o Parlamento possa ter na opinio pblica. +Podemos dizer coisas importantes, tomar grandes decises, mas se no tiver repercusso... +Tudo depende da capacidade que o Parlamento tiver de transmitir as suas idias imprensa, ao rdio e televiso. +No sei se os jornalistas esto conscientes da grande responsabilidade que tm. +Os seus 100 quilmetros j levaram este ano 200 mil toneladas de soja. +Custo: US$ 8 por tonelada. +No Sul, US$ 24! +Por que no publicar que o PT fez um seminrio no Nordeste sobre ela e concluiu pela necessidade de sua construo? +Esta uma obra do Brasil, transformadora da estrutura do pas. +No comporta qualquer reserva, e bendito o pas em que os homens pblicos exigem dos candidatos a soluo de problemas nacionais, minorar a fome, dando leite s crianas que se alimentam de lixo, e estradas para desenvolver o pas. +O ex-jogador assumiu o cargo ontem em substituio a Carlinhos, demitido aps a derrota para o Bahia (0 a 1), anteontem. +As torcidas partem para cima da PM a fim de que nos concentremos num s local. +Entre os denunciados por corrupo passiva esto o procurador de Justia aposentado Aldegy do Nascimento, 26 delegados, inspetores e peritos da Polcia Civil e Ary Chagas de Aguiar, assessor da promotora Lcia Atalla. +Os 38 nomes fazem parte de um aditamento primeira denncia feita pelo Ministrio Pblico no escndalo do bicho, em junho. +O pai de Romrio foi sequestrado na segunda-feira noite, no Rio. +Os sequestradores estariam exigindo US$ 7 milhes (cerca de CR$ 9,6 bilhes) para libert-lo. +Abalado, o jogador ameaou no disputar a Copa do Mundo, em junho e julho, nos EUA, caso o pai no seja libertado. +O stimo filme de Spike Lee acerta nos figurinos e trejeitos da poca e resgata preciosidades do soul negro dos anos 70. +As msicas incluem The Jackson 5, Stevie Wonder Jimi Hendrix, James Brown e outros 18 bons nomes. +O problema a histria. +Quase nada acontece. +H algumas brigas engraadas entre irmos, cenas de separao conjugal e uma morte. +Em algumas passagens, h dilogos interminveis e vazios. +Durante o almoo familiar, por exemplo, fala-se muito sobre quase nada e ningum mastiga. +Soa forado demais. +No se sabe ainda como Oosterbroek morreu. +Seu corpo no tinha marcas de tiros. +Aparentemente ele quebrou o pescoo quando tentava fugir do fogo cruzado. +A polcia anunciou a libertao de seis homens que estavam trancados no poro de um prdio do CNA em Johannesburgo. +Segundo o porta-voz da polcia, Dave Bruce, os seis foram torturados. +O deputado Adroaldo Streck (PSDB-RS) ficou encarregado de confirmar com Britto o encontro. +Britto j informou a Streck que favorvel, mas ficou de discutir a questo com o PMDB gacho. +Camila viajava no colo de Dalva, na frente do veculo. +O corpo de Camila foi lanado para fora do carro. +A menina morreu na hora. +Dalva seria submetida ontem a cirurgia no hospital de So Paulo. +A famla voltava para casa, em Americanpolis, depois de participar de casamento na Barra Funda e deu carona a amigos. +Outras cinco pessoas ficaram feridas. +Niemeyer -- , foi em 1936. +Folha -- o que o sr. pode dizer dele em termos pessoais? +Jazz rap? +Esquea. +Ou melhor, no esquea porque ainda se faz muita coisa boa, mas deixe de querer ser bacana citando-o como a ltima tendncia. +Para aquele verniz atualizador, passe desde j a usar a expresso blues rap. +E, para um efeito extra, cite G. Love and Special Sauce. +Seu lbum de estria, lanado no ltimo vero americano, causou pouco ou nenhum impacto na parada. +Mas uma boa escavao nas sees de crticas de revistas de msica importadas revela a verdadeira adorao que G. Love estimulou entre os jornalistas especializados. +O secretrio-geral da Presidncia, Mauro Durante, tambm tem emprego garantido. +Itamar quis encaix-lo no Tribunal Superior do Trabalho, mas a OAB (Ordem dos Advogados do Brasil) no deixou. +O presidente articulou, ento, lobby certeiro em direo ao Sebrae (Servio de Apoio Pequena e Mdia Empresa) e ainda pediu a FHC que o ajudasse a pr Durante na presidncia da entidade. +Esses dados fazem parte do livro Conflitos no Campo Brasil 1993, lanado na ABI (Associao Brasileira de Imprensa), no Rio de Janeiro, pela CPT (Comisso Pastoral da Terra), rgo da Igreja. +Constam do livro 15 quadros estatsticos relacionando os nomes das vtimas e os responsveis por crimes ligados a conflitos de terra. +As causas da violncia no campo tambm so discutidas. +O diretor executivo do FMI tambm enfatizou a necessidade da queda dos juros no Brasil para incentivar a volta de investimentos macios nos setor produtivo. +A uma pergunta sobre se a volta dos investimentos externos no dependeria do selo de aprovao do Plano Real atravs de um acordo formal entre o FMI e o Brasil, Camdessus disse que o que vai fazer a taxa de juros cair no um acordo com o FMI, mas a credibilidade interna e externa do Plano Real. +Bancos e financeiras receberam bem a resoluo 2.071 do Conselho Monetrio Nacional. +Baixada sexta-feira, ela cria a taxa de juro flutuante. +Abre a perspectiva de aplicaes por prazo mais longos. +O juro flutante permite reavaliar periodicamente as taxas de juros das aplicaes e emprstimos. +A interveno diplomtica russa foi decisiva para evitar o envolvimento ocidental na guerra civil na Bsnia. +Ao se oferecer para intermediar o conflito, a Rssia deu aos seus aliados srvios a possibilidade de uma retirada honrosa. +Isso praticamente eliminou a ameaa de ataques areos da Otan. +Essa a avaliao do Instituto Internacional de Estudos Estratgicos de Londres (IIEE), um dos mais importantes centros de pesquisa militar do Ocidente. +Um ataque agora no faz mais sentido, desde que os srvios continuem a recuar, disse Folha o coronel Andrew Duncan, do IIEE. +O espantoso que fora da polcia tambm se viu o mal no incidente de Cavalera. +O jornal carioca O Globo recolheu depoimentos sobre a pisada no confirmada nem pela polcia e explicada por Cavalera como um simples tropeo, normal na movimentao de palco, em seu depoimento na delegacia. +Todos, contra ou a favor, partem do princpio de que aquele foi um gesto de protesto. +Foi um protesto moleque. +S poderia ter partido de uma cabea de merda. +Um psicanalista (sic), Jos Nazar, sugere, acreditem se quiser, nada mais nada menos do que linchamento como punio. +O jurista Miguel Reale Jr. prefere um pessedebismo pedaggico, ao aprovar a atitude da polcia em det-lo como exemplo para a juventude. +A tumorectomia garante sobrevida comparvel e melhor qualidade de vida do que a mastectomia total, escreveu a chefe do novo estudo, Anna Lee-Feldstein. +Cientistas da Universidade da Califrnia em Irvine (EUA) publicaram a pesquisa na ltima edio da revista da Associao Mdica Norte-americana, (Jama). +Com isso, o Cruzeiro deve comear a partida com Michelli, Magno, Derlan, Marcus Vincius e Anderson; Emiliano, Juliano e Anderson Leo; Ricardinho, Nlson e Herbert. +A equipe vem treinando junta h pouco tempo. +O atacante Nlson e meia Anderson Leo chegaram agora ao time e o ponta-direita Ricardinho, o meia Juliano e o zagueiro Derlan foram promovidos no final de 93. +Recm-promovido, Derlan, 17, j um dos destaques do Cruzeiro por sua habilidade nas sadas da defesa para o ataque. +O jogador chegou no clube h dez meses e, em novembro, foi para a equipe de juniores. +Estamos muito motivados. +A casa vai estar cheia e ns vamos mostrar porque viemos disputar a Copa, afirma. +Como sua musa Iris gosta do tema, Silvio Santos vai agora todos os weekends ao teatro. +Depois da pea de Leilah Assumpo, os dois foram juntinhos assitir a Aluga-se Um Namorado. +Amaram. +Pargrafo 3. -- Nos contratos celebrados ou convertidos em URV, em que haja clusula de reajuste de valor por ndice de preos ou por ndice que reflita a variao ponderada dos custos dos insumos utilizados, o clculo desses ndices, para efeitos de reajuste, dever ser nesta moeda at a emisso do real e, da em diante, em real, observado o art. 38 da lei n. 8.880 de 27.05.94. +Pargrafo 4. -- A Taxa Referencial -- TR -- somente poder ser utilizada nas operaes realizadas nos mercados financeiro, de valores mobilirios, de seguros, de previdncia privada e de futuros. +A sensao que se tem quando se est andando num tren puxado por ces semelhante do esqui aqutico. +Na minha primeira tentativa, fiz exatamente a mesma coisa que na primeira vez que pratiquei esqui aqutico: ca e me soltei. +Precisei cair mais algumas vezes at aprender a usar o breque. +Alm de dominar esta habilidade, as duas nicas coisas realmente necessrias para se viajar de tren pelo Alasca eram estar em razovel boa forma fsica e ter mais de 13 anos de idade. +Eu e meu marido, advogados de meia idade, apenas um pouco fora de forma, satisfazamos essas condies. +Todos os plnios costumam almoar no Esplanada Grill, refeies de US$ 40, em mdia. +Os motivos vo do bom atendimento certeza de achar ali pessoas como ns. +E se encontram noite no bar Cabral, embora no se conheam. +No necessrio. +Os plnios tm no Cabral, aberto no final de 1992 numa travessa escondida da avenida Cidade Jardim, sua meca. +Luciano Huck, 22 um dos proprietrios do bar, o profeta da turma. +L, os plnios fazem amigos, influenciam pessoas, revem colegas, contam as novidades -- as ltimas viagens para os Estados Unidos, quem comprou carro novo, quem vai ter que comear a trabalhar com o pai, os que ainda no conseguiram telefone celular. +Pedem cerveja, tequila e batidas, num gasto mdio de US$ 30 por noite, e falam mal dos mauricinhos que, como diz Rubinho Gimenes, so cheios de querer ser. +Eles no. +Eles so. +O jovem corretor de seguros Kiko Villela, 22, . +Cursando economia na Faap, Kiko espera ansioso o seu telefone celular, que est para sair. + imprescindvel hoje em dia, regulamenta. +O brinquedinho eletrnico tem uma funo interessante no mundo dos plnios: substitui o antigo torpedo, bilhete desferido entre jovens em bares. +Em vez de mandar um papel pelo garom com cantadas escritas, o plnio liga para a plnia de sua predileo e pronto -- est feito o contato imediato. +Entretanto, h possibilidade de queda nas cotaes externas nos prximos meses, em especial no segundo semestre, quando se verifica a colheita de arroz no hemisfrio Norte. +A queda das cotaes internacionais viabilizaria o ingresso do produto a preos mais baixos, fato que impediria maiores avanos das cotaes internas. +Segundo a previso da Conab, a produo nacional de arroz deve chegar a 10,6 milhes de toneladas de gros. +No ano passado, o pas colheu 9,5 milhes de toneladas. +O diretor-presidente da Yashica do Brasil, Kazuo Tamura, 56, morreu em um acidente de carro ontem de manh em So Paulo. +Ele dirigia sozinho um Santana 90 em alta velocidade na avenida do Estado, no Cambuci (regio central), quando, provavelmente, perdeu o controle do carro e bateu num poste. +Isoladamente, os EUA e o Japo, por exemplo, praticaram alquotas ainda menores 5,46% e 4,79%, respectivamente. +O levantamento mostra que a Constituio de 1988 elevou de 15,27% para 20,53% a alquota mdia sobre o consumo no Brasil em 1989. +O ministro da Aeronutica, Llio Viana Lbo, aproveitou a comemorao do Dia do Soldado ontem para queixar-se das dificuldades salariais dos militares. +Vivemos ainda dias difceis, num contexto econmico, social e poltico em que as necessidades bsicas de nossas foras ( ...) tm sofrido severamente as agruras que toda nao atravessa, disse, em saudao enviada ao Exrcito. +Os fs de Frank Zappa vo ter uma boa surpresa em janeiro. +O lanamento de um CD com uma coletnea de msicas de Zappa interpretadas por bandas cover dos Estados Unidos, Alemanha, Blgica, Inglaterra, Holanda, Itlia, Sucia e Brasil (com a banda The Central Scrutinizer). +A banda brasileira nasceu em 1990 para fazer uma homenagem a Zappa. +Era um cara que sempre apostou no que acreditava. +Lutava contra a bestialidade das pessoas e sempre mostrou isso de uma forma bizarra, conta Mano Bap, 29, vocalista da The Central Scrutinizer. +Para o diretor de crdito da Febraban, Christoph Heinrich Von Beackedorff, no h novos tomadores, s esto sendo rolados os emprstimos que esto vencendo. +Ambos apontam o baixo volume de captao de recursos com a venda de CDBs aos investidores como o principal fator de insegurana dos bancos na definio dos custos dos emprstimos. +Desse modo, apenas parte do fundo de viagem seria empregada na compra de traveler cheques ou cdulas. +O restante ficaria em uma aplicao com aniversrio coincidindo com o vencimento da fatura mensal do carto. +O caso passou a ser investigado porque pessoas que foram ao velrio de Pereira teriam dito que o cadver suava e teve sua pele arrepiada dentro do caixo. +A famlia foi alertada mas no chamou nenhum mdico e fez o enterro depois de dez horas de velrio. +No cinema, sorte do espectador que ter no uma, mas duas mostras internacionais: a Internacional de Cinema, que agora vai ser competitiva e tem Pedro Almodvar e Quentin Tarantino entre os convidados, e a Banco Nacional de Cinema, que ser mais que uma amostra do evento com sede no Rio, com um ciclo em homenagem ao rei do filme B, Roger Corman, tambm presente. +Shows h para todos os ouvidos. +O 9 Free Jazz Festival ampliou seu leque e programou uma noite trepidante sob o comando do godfather of soul James Brown e cedeu espao para a modernidade de Guru, US3 e dos Digable Planets. +Fora do festival, o De La Soul a atrao mais promissora. +Como eles aumentaram 6,28% nas duas semanas, acertamos que eles iriam parar os aumentos. +Dallari recebeu tambm a Associao Brasileira da Indstria de Mquinas. +A entidade tambm se queixou de aumentos que variam de 5,5% a 11,5% no custo de alguns fundidos, forjados, ao plano e rolamentos. +Foi ao final de um ano e trs meses que o prefeito teve sua melhor avaliao. +Recebeu 27% de timo e bom, 30% de ruim e pssimo e 42% de avaliao regular. +A boa performance de Maluf em maro ltimo coincidiu com a fase em que ele anunciava sua candidatura presidncia e investia em inauguraes de grandes obras, como o tnel do rio Pinheiros. +A Gelaguela maior fabricante nacional de sobremesas individuais geladas est investindo US$ 200 mil no sistema de franquias. +A empresa, que faturou US$ 1 milho em 93, vai inaugurar at setembro mais quatro unidades da Gelaguela Sobremesas. +O objetivo consolidar a marca. +Esta taxa, no entanto, no reflete o que est acontecendo de fato com os preos nas ltimas semanas. +Isto porque os ndices de inflao s captam totalmente as mudanas de preos 30 dias aps o aumento. + sempre considerada a mdia de quatro semanas em relao s quatro anteriores. +A partir de agora, a taxa deve sempre recuar. +Os preos estiveram praticamente estveis nas ltimas semanas. +Juarez Rizzieri, coordenador do ndice de Preos ao Consumidor da Fipe, prev para agosto uma taxa entre 1% e 2%. +A inteno votar e aprovar a MP na prxima semana, entre os dias 26 e 27. +O prprio presidente Itamar Franco pedir empenho de todos os ministros para mobilizar os seus partidos para a votao. +O ministro Ricupero ficou satisfeito com aprovao da MP da URV na comisso mista do Congresso que examinou a medida. +Para o ministro, disse Simon, as mudanas feitas so assimilveis pelo plano. +Os novos proprietrios sero integrados rede de franquia da Pakalolo, que conta atualmente com 76 pontos de venda. +Tambm sero submetidas ao mesmo processo as 28 lojas da Body for Sure, a grife esportiva do grupo. +Eu acredito no efeito barriga no balco, afirma o empresrio Humberto Nastari, 37. +Na condio de senhor de meia-idade com idias adolescentes, tio Dave vem observando com bastante inquietao que seu sentido do paladar parece estar se esvaindo pelo ralo. +Peixe passou a ter gosto de carne para mim e uma noite destas, no Rodeio, eu confundi salsicha caseira com frango kebab. +Chocante. +Constrangedor. +Ento me deparei com um estudo que afirma que algumas pessoas realmente tm um paladar mais aguado do que outras. +E, horror dos horrores, so as mulheres que tm o paladar mais apurado do que os homens (pare de dar essas risadinhas, voc a no fundo da classe). +Parece que a Universidade Yale, aquela que nos deu o inesquecvel George Bush, comprovou a existncia de superdegustadores, ou seja pessoas que possuem um nmero de papilas gustativas muito alm do normal. +Adventure in Castle um programa para aprender matemtica. +Em ingls, traz noes sobre clculos de soma, subtrao, multiplicao e diviso. +Custa US$49, na Brasoftware. +Tel. (011) 253-1588. +Journey in the Universe um programa em ingls, voltado para crianas que querem aprender noes de astronomia. +Est sendo vendido na Brasoftware em promoo. +Sai de US$ 79 por US$ 49. +MacMillan Dictionary for Children um dicionrio de ingls em CD-ROM. +Conta com recursos de figuras e sons, que mostram o significado das palavras. +Custa US$45 na Multimdia Center (tel 011 959-2650). +Ele disse, no entanto, que a lei antitruste aprovada pelo Congresso reflete o clima que o pas est vivendo nessa fase que antecede o lanamento do real. +Mas afirmou que a lei antitruste aprovada pelo Congresso tem as deformaes tpicas da situao conjuntural. +Apesar da declarao dos separatistas, o governo britnico continua disposto a iniciar as negociaes com o Sinn Fein, brao poltico do IRA, antes do Natal. +O ministro britnico para a Irlanda do Norte, Patrick Mayhew, afirmou ontem em Belfast que o cronograma do processo de paz ser mantido. +Mayhew disse que o IRA poderia demonstrar que realmente no autorizou a operao de Newry devolvendo os US$ 210 mil levados durante o assalto. +FHC -- Esse comit faz tudo errado. +Ruth -- Por isso perdemos as eleies. +(FHC comea a rir). +Sabe por que eu amo tanto voc, querido? +Porque est sempre rindo. +(Comeam ambos a chorar. +Um estudo do Corecon (Conselho Regional de Economia), do Rio de Janeiro, divulgado ontem, est projetando, na hiptese de adoo do real em abril, uma inflao de 2,3$%. +Com base nesta projeo, o estudo prev uma inflao com a nova moeda de 30% em 94. +O SPQ (Sistema de Projees Qualificadas) resulta da mdia de opinies de 16 economistas que fazem anlise de conjuntura. +Segundo o presidente do Corecon, Hlio Portocarrero, a inflao do real no ser puxada pelos oligoplios, apontados pela equipe econmica do governo como os principais viles na subida de preos aps a implantao da URV. +Para ele, a alta de preos na fase da nova moeda dever ser impulsionada pelos setores competitivos como o agrcola e o de servios. +O projeto ainda no oficial. +Segundo o professor Vicente Amato Neto, 66, h informaes que todos ouvem, no sentido de fazer o estacionamento. +Ontem, a diretoria da atltica e Amato Neto que iniciou a organizao dos alunos para a preservao do clube se reuniram para discutir alternativas para a rea. +Cedras no tem o controle de sua polcia. +Para evitar distrbios, vamos distribuir s TVs uma fita para mostrar populao o que viemos fazer aqui. +Esperamos uma mudana de atitude da polcia. +Esto ocorrendo violaes dos direitos humanos. +Se o sr. Cedras no tomar os passos apropriados, ns vamos dizer num clima de respeito que passos ele deve dar. +Tambm vamos distribuir 1 milho de cestas bsicas, para matar a fome da populao at a transio para a democracia. +A Roma venceu ontem a Lazio, tambm na capital italiana, por 3 a 0, na partida mais importante e empolgante da dcima primeira rodada do Campeonato Italiano de 1994/95. +Animado com um teste de vestirio, o tcheco Zdenek Zeman, tcnico da Lazio, colocou em campo o atacante croata Alan Boksic, cuja volta ao time estava prevista para a prxima semana. +No mercado futuro do ndice Bovespa, a cotao para maio ficou em 20.000 pontos, projetando rentabilidade de 66,78% ao ms. +No mercado futuro de dlar, a expectativa de desvalorizao cambial para abril ficou em 42,40%, contra 42,53% no dia anterior. +O comportamento das pessoas no trabalho est passando por uma nova e importante alterao. +Gesto participativa, times de qualidade e outros conceitos foram assimilados pelas empresas ao mesmo tempo em que elas passaram a exigir funcionrios mais abertos ao dilogo e cientes da importncia do cumprimento das regras de boas maneiras dentro do ambiente de trabalho. +As empresas notaram que sua imagem a maior prejudicada quando um funcionrio comete uma indelicadeza em pblico e diante de um cliente ou subordinado, afirma o consultor de marketing pessoal Otto Reiter, 69. +O refinamento do executivo mais um diferencial na hora da sua avaliao, completa. +Nos Estados Unidos, universidades e algumas empresas chegam a oferecer cursos de boas maneiras nos negcios. +No Brasil, essa atitude ainda depende da iniciativa de cada um. +A questo que enfrentaram era retomar a arte intensamente dramtica de sua tradio sem se escravizar a ela. +A soluo que deram, obter equilbrio entre figurao e abstrao em que a profundidade no nem clssica (pers -- pectiva) nem modernista (planificada), mas sugerida por jogos cromticos semilivres s vezes livres demais, como voc vai ver. +Cada um o fez a seu modo. +Dos cinco, Baselitz, Immendorff e Kirkeby fizeram melhor. +Kirkeby, que vem ao Brasil para a abertura, tambm ter sala especial na 22 Bienal, a partir do dia 12. +Tanta coincidncia s pode ser instrutiva; pode mostrar mais a Alemanha que qualquer contato poltico afinal, como dizem eles, o ser reside na linguagem. +Habite-se. +Alm da proposta de 18 cassaes, o relatrio final da CPI sugere que oito deles sejam objeto de processo criminal. +So os deputados que, segundo o texto, devem ter os elementos comprobatrios encaminhados ao Ministrio Pblico. +Esto nesse grupo Ricardo Fiuza (PFL-PE), Joo Alves (sem partido-BA), Cid Carvalho (PMDB-MA), Jos Geraldo (PMDB-MG), Manoel Moreira (PMDB-SP), Fbio Raunheitti (PTB-RJ), zio Ferreira (PFL-AM) e Paulo Portugal (PP-RJ). +Alm disso, avaliou como fraca a organizao existente at ontem na campanha. +A partir da prxima semana, vo viajar para os Estados os membros da coordenao que no pertenceram ao ncleo que passa a deter as decises. +Cerca de 300 pessoas foram convidadas para a cerimnia. +De esse total, 40 so parentes e amigos do ex-jogador. +Entre os convidados esto o rei da soja, Olacyr de Moraes, o empresrio Alfredo Saad e o comandante Rolim Amaro, dono da empresa area TAM. +Os funcionrios da prefeitura reclamam que Ldice est adiando desde maio o pagamento das perdas salariais da categoria. +Eles tentaram agendar um encontro com Lula para pedir que ele ajudasse na negociao. +Uma reproduo gigantesca da Maja Desnuda, de Goya, ocupa uma das paredes da casa. +No banheiro, um funcionrio vende balas, cigarros, chocolates, gua mineral, remdios (Engov) e, claro, camisinhas. +4 de novembro -- morto em emboscada o lder da ALN e ex-deputado Carlos Marighella. +19 de junho -- Mdici anuncia a construo da rodovia Transamaznica. +As questes colocadas pela body art nos anos 60 foram retomadas agora como reflexo sobre uma realidade em que os corpos esto submetidos a um trabalho de transformao que reala o sentido trgico. +A Aids foi apenas um catalisador para a volta do prprio corpo como objeto esttico em diversos trabalhos. +O vdeo uma experincia fsica, ( ...) tem um efeito direto nos corpos das pessoas. +O vdeo pode ser um instrumento poderoso para tocar as pessoas diretamente, na percepo, em reas que a cultura ocidental no leva em conta como um caminho para o conhecimento. +Desde a Idade Mdia, esse caminho feito atravs do intelecto e no do corpo. +O corpo foi negligenciado, disse Viola Folha em 92. +A ao alega que a Petroplastic no considera a Dow acionista da Triunfo. +E comprova que a Petroplastic recebeu US$ 5 milhes para concordar com a transferncia, para a Dow, do controle da Atochem (fundadora da Triunfo). +Boris Gorentzvaig, da Petroplastic, diz que os US$ 5 milhes eram adiantamento em acordo de gaveta com a Dow. +Mas a Dow dos EUA no quis cumprir o contrato e transferir tecnologia a uma concorrente, diz. +O rgo responsvel pelo mercado de telebingos a Susep (Superintendncia de Seguros Privados), uma autarquia federal subordinanda ao Ministrio da Fazenda. +Compramos o ttulo para verificar, levei para casa e no enxerguei nada, disse ontem Vera Melo Arajo, chefe do Departamento de Fiscalizao da Susep. +Fbio Igel, Joo Herreras e Marcelo Loureiro pilotam festa no Cabral. + em prol do Fundo Social de Solidariedade a apresentao de hoje de Mulher, a Melhor Opo de Investimento no Teatro Itlia. +Divulgado na semana passada, o fraco resultado em novembro da produo industrial e das novas encomendas s indstrias acabou com as esperanas de que a Alemanha tivesse sado da pior recesso do ps-guerra no ltimo trimestre do ano. +Os dados detalhados sobre o desempenho do PIB entre outubro e dezembro no sero revelados at maro. +Os economistas afirmam que a recuperao puxada pelas exportaes que o governo previa para o segundo semestre de 1993 no aconteceu. +A federao das pequenas e mdias empresas revelou numa pesquisa que seus filiados esto prximos do pnico em relao situao econmica. +Suas expectativas para o futuro prximo da economia so as piores em dez anos. +No preciso, governador, porque, sabendo da hospitalidade paranaense, eu nem trouxe carteira. +Os anos 80 foram um divisor de guas na industrializao brasileira. +Vera Lcia Barbosa, 40, morreu na ltima quarta presa pelo cinto de segurana ao carro em que estava, que caiu no rio Tiet. +O acidente foi s 21h50, perto da Ponte Cruzeiro do Sul (zona norte). +As outras trs pessoas que estavam no carro conseguiram se salvar. +O tombamento de um caminho no km 226 da pista Rio-So Paulo da via Dutra s 5h de ontem provocou engarrafamento na rodovia. +O acidente causou a morte do menino Ren Scorsa, que estava no Logus dirigido por Srgio Duran, 33, que bateu no caminho. +Sobral, embora seja uma das principais cidades do Cear, ainda no dispe de um sistema adequado de esgotos. +O projeto de canalizao de um crrego no municpio de Contagem ( ...) indispensvel para os moradores porque, durante o perodo de chuvas, o rio transborda e alaga alguns bairros de Contagem. +Nenhum municpio brasileiro, nem mesmo So Paulo, dispe de um sistema adequado de esgotos. +Enchentes do-se pelo pas inteiro. +A escolha to particular de Sobral, que por ser uma das principais cidades do Cear tambm uma grande concentrao eleitoral do Estado, coincide com sua escolha para a ao de campanha que l fez o candidato Fernando Henrique no fim-de-semana. +A mineira Contagem outra grande concentrao eleitoral, com prefeito do PSDB e onde o partido, j como propaganda, fez a conveno de escolha de Fernando Henrique. +Covas -- Quando se falando em emprego, no se tratando s de uma questo administrativa. +Quando se fala do Real, fala-se de posies que traduzem aquilo que deve ser o papel de So Paulo. +Folha -- Pases como a Argentina, que passaram por ajustes parecidos com o do Brasil, tm hoje um desemprego recorde ... +Qual a matria odiada na escola? +Lugar de que mais gosta? +Os dados so do SCI (Servio de Segurana ao Crdito e Informaes) que armazena informaes comerciais sobre 1,6 milho de empresas no Brasil. +Do volume de protestos, a regio Sudeste respondeu por 39,1% do total e a Sul, 23,5%. +O Estado de So Paulo representou 22,8% dos protestos. +O conselho tambm manteve multas aplicadas Indstrias J.B. Duarte e a seus dirigentes pela prtica de atividades restritas a instituies financeiras. +A corretora Antonio Delapieve e seus dirigentes foram multados por negociar ttulos de renda fixa a preos superavaliados. +Hoje, os mediadores internacionais lorde Carrington e Henry Kissinger (ex-ministros do exterior do Reino Unido e EUA, respectivamente) se encontram com os lderes rivais negros Nelson Mandela (CNA) e Mangosuthu Buthelezi (zulu). +Pelo menos dez pessoas morreram e 80 se feriram no terceiro dia consecutivo de luta entre faces rivais em Kabul, Afeganisto. +Tropas do presidente Burhanuddin Rabbani enfrentam soldados do primeiro-ministro Gulbuddin Hekmatyar. +Biotecnologia -- busca o aprimoramento e criao de novos produtos a partir da manipulao de animais e plantas destinados sade e esttica humana. +Apesar de ter conseguido a guarda de Natasha, Conceio est aflita. +Tenho os papis, mas no tenho a minha filha. +Agora s deixo a Frana com ela. +Esse o meu grito, se inflama. +Segundo Conceio, tem existido certa indiferena da polcia local em relao ao caso. +A estratgia montada por Qurcia tem o objetivo de ganhar a prvia do PMDB que vai definir o candidato do partido Presidncia no prximo dia 15, bem como manter o PMDB unido durante a campanha. +Os ataques a Sarney s sero feitos se a contabilidade quercista constatar que a vitria na prvia est ameaada. +Esse o tema mais discutido na imprensa do pas; anteontem, ele foi objeto de debate especial do gabinete de governo japons. +No caso de ontem, um garoto de 14 anos foi encontrado enforcado numa viga em sua escola em Yorii, 50 km a sudoeste de Tquio. +A RBS tem ainda trs concesses de TV em Santa Catarina e 30% da TV Cachoeiro, de Cachoeiro do Itapemirim, no Esprito Santo. +O nico caso que se tem registro de aplicao rigorosa da lei ocorreu no final da dcada de 60. +Destaca que o Partido Liberal Democrtico (PLD), derrubado do poder no ano passado, dever apresentar seu presidente, Yohei Kono, como candidato a primeiro-ministro do Japo. +Publica documentos comprados de James McDougal, ex-scio de Bill e Hillary Clinton na empresa Whitewater. +Os documentos indicam que a primeira-dama dos EUA estava mais envolvida no dia-a-dia da administrao da firma do que fora admitido at agora. +Depois de uma disputa acirrada entre as indstrias para conquistar os melhores mercados, o ranking das gigantes multinacionais que dividem o bolo mudou. +A japonesa NEC continua na liderana. +O segundo lugar foi conquistado pela Ericsson, que em agosto de 93 ocupava a quarta posio em contratos assinados para fornecimento de equipamentos. +A NEC tem mais de 55% (US$ 455 milhes) dos contratos assinados pelas concessionrias da Telebrs, incluindo a a segunda fase da implantao do sistema mvel em So Paulo e a terceira no Rio, previstas para 94. +O engenheiro Bruno Maranho, lder do Movimento por Terra, Trabalho e Liberdade, pertence a uma das famlias que dominam os latifndios no Nordeste. +Uma invaso do grupo, feita h dois anos, em Pernambuco, na usina Massauassu, prejudicou justamente a famlia Maranho. +Depois de conquistar seu prmio de estria no concurso Smirnoff em So Paulo, o jovem estilista Srgio Machado emplacou mais uma. + dele o segundo lugar do Smirnoff International Fashion Awards, disputado anteontem em Dublin. +Alphaville uma mistura de fico cientfica, filme noir, e a irreverncia das histrias em quadrinhos. +A histria narra a ida de um agente secreto desumana cidade futurista de Alphaville, controlada por um crebro eletrnico que baniu totalmente os conceitos de amor e solidariedade. +Para construir o ambiente futurista do filme, Godard recorreu quase que totalmente fotografia. +A cmera de Raoul Coutard conseguiu transformar Paris numa cidade glida, sem contar a ousadia de montar sequncias inteiras em negativo. +O experimentalismo do filme no esconde sua proposio simblica, a alienao na sociedade tecnolgica. +Uma audincia hoje na 7 Junta da Justia do Trabalho em Belm reabre a discusso sobre a obrigatoriedade do diploma especfico para o exerccio do jornalismo. +O Sindicato dos Jornalistas do Par exige a demisso dos cerca de 150 jornalistas irregulares (que no tm curso universitrio de Jornalismo) das empresas de Belm. +Como o Estado no tem recursos, nem vai ter para todos, o atendimento acaba deixando de fora justamente os mais pobres, que no conseguem acesso aos servios pblicos. +O problema poltico porque envolve, por exemplo, a gratuidade da educao, da sade, da previdncia mnima. +Excetuada a educao bsica de primeiro grau, que deve ser gratuita e obrigatria, para todo o resto preciso discriminar quem pode pagar. +Dados do IBGE (Instituto Brasileiro de Geografia e Estatstica), de 1989, mostram que 30,6 milhes de brasileiros. +Dos fumantes, 18,1 milhes so homens e 12,5 milhes, mulheres. +O homem fuma entre 11 e 20 cigarros por dia e a mulher, entre 5 e 10. +A PF (Polcia Federal) indiciou ontem duas pessoas por vender carro com gio. +Foram indiciados o dono da revendedora 3.000 Automveis, Leonardo Romanioli Filho, e o vendedor Lus Silveira, da concessionria Frame. +Os dois foram presos quando o taxista Jos Fioravanti, 61, foi entregar US$ 1.000 a Romanioli como gio da compra de um Santana Quantum. +Ele estava acompanhados de trs agentes da PF. +Um deles era seu filho, Srgio Fioravanti. +A partir desta edio, a tabela com o levantamento dos indicadores Folha-Sebrae passa a incluir a comparao da capacidade ocupada no ms pesquisado com o mesmo ms no ano anterior. +O uso da comparao anualizada permite verificar a variao da atividade industrial sem a influncia de fatores sazonais. +Com a incluso de mais esse dado comparativo, a tabela fica mais completa. +Ela tem tambm textos com explicaes sobre cada item pesquisado. +A verso mais corrente sobre o incio do enriquecimento de Qurcia de que ele comprou terrenos em reas de periferia que seriam beneficiadas pelos projetos de melhoria urbana da Prefeitura. +Na sua administrao, executou um plano virio com largas avenidas interligando os bairros, que provocaram um boom imobilirio. +Nicolau diz que hoje o produtor de leite C recebe US$ 0,22, enquanto o custo est em US$ 0,24. +Dallari orientou os produtores a negociarem com a indstria. +Esses aspectos podem ser observados nos relatos das pacientes: quando tenho que arrumar a casa e no estou com vontade, escuto vozes que me incentivam a comear a tarefa. +Depois de um tempo, as vozes aumentam e acabo ficando atordoada e no consigo terminar o que comecei. +Estes problemas no so frequentes nos homens, pois alm de terem uma possibilidade menor de casarem e terem filhos, esses no so os tipos de papis socialmente esperados para o sexo masculino. +O restaurante, especializado em carnes e saladas, oferecer uma garrafa de champanhe espanhol para os casais na noite do dia 12. +Quem no quiser beber durante o jantar, poder levar o champanhe para casa. +As pessoas esto cansadas do hype que se cria em torno das roupas, dos estilistas e do luxo abusivo. +Conforto fundamental. +Eu no me associo com moda. +Para mim roupa e moda so coisas diferentes. +Folha -- o que a mulher precisa hoje, em termos de roupas? +Desorganizao e violncia marcam torneio que consolida o 'efeito Copa'. +Os clubes de futebol do Brasil esto valorizando cada vez mais a posse de bola e a utilizao de lanamentos como armas para se chegar ao ataque. + o que confirmam as estatsticas do Campeonato Brasileiro, encerrado ontem com o novo ttulo palmeirense. +Emily Lloyd vai ser a desbocada punk Tank Girl, dos gibis ingleses. +Ainda na Inglaterra, Spielberg negocia os direitos da srie de TV Doctor Who. +Chris Columbus est ligado adaptao dos super-heris Quarteto Fantstico, que inauguraram a era Marvel nos gibis h 30 anos. +Mas o oramento de US$ 5 milhes insinua que a informao no sria. +So comuns casos como a filmagem do gibi Sargento Rock, que teve os direitos comprados por Joel Silver h cinco anos. +Arnold Schwarzenegger deu entrevistas como ator principal, foi substitudo por Bruce Willis h dois anos, mas o projeto nunca saiu do papel. +Na campanha eleitoral, Alencar e Nilo estiveram em lados opostos quanto ao das Foras Armadas no combate ao crime. +Alencar defendeu a adoo do estado de defesa, que prev a suspenso de garantias individuais. +Trata-se, segundo ele, de um instrumento de defesa da democracia. +Ao contrrio, nesta situao econmica de extrema gravidade, todos gostariam de ajudar o pas. +Para isso, porm, necessrio que o nosso Banco Central busque no a independncia do Tesouro, que impossvel, mas que deixe de quebrar-lo com a sua poltica agressiva de juros. +O shopping West Plaza j comeou sua promoo de Dia das Mes. +Diariamente, est promovendo desfiles de moda para seus consumidores. +Os desfiles, com roupas a venda no prprio shopping, visam orientar o consumidor a respeito da moda. +Estilistas estaro disposio dos clientes. +Segundo ele, isso aconteceu aps a priso de soldados venezuelanos em territrio brasileiro, em fevereiro de 1993. +Depois desse incidente, as Foras Armadas venezuelanas tiveram de esfriar suas aes na fronteira com o Brasil. +A Guarda Nacional da Venezuela passou a conversar com os ndios e a convenc-los de fazer o papel dela, ou seja, reprimir os garimpeiros, diz Altino. +A maioria dos bons jogadores brasileiros atuava em clubes profissionais, filiados FBF. +No houve acordo para uma trgua durante a Copa. +A soluo foi negociar diretamente com os jogadores. +A CBD aliciou Lenidas (Vasco), Luizinho e Waldemar de Brito (So Paulo), entre outros. +Era uma poca em que os cariocas imperavam em campo e fora dele. +Por razes que a prpria razo desconhece, no sorteio do local do jogo decisivo, a bolinha quase sempre apontava o assustador estdio de So Janurio. +O torcedor paulista sonhava com uma era futura, em que So Paulo chegaria supremacia, se possvel de forma arrasadora. +Passaram-se uns 50 anos e o sonho se realizou, trazendo alegria e tambm, inesperadamente, frustrao. +Ficamos todos , isto , os torcedores paulistas que vem dos idos de 1940, como o americano mdio, no fim da Guerra Fria. +J no h o imprio do mal para combater. +Vasco, Botafogo, Fluminense e at o Flamengo so fantasmas do passado. +O bombeiro suspeita que o golfinho tenha morrido afogado. +Ele teria ficado preso em uma rede de pesca sem condies de subir tona para respirar. +Os bombeiros pediram ajuda ao Cebimar (Centro de Biologia Marinha) da USP para apurar as causas da morte do golfinho. +At as 17h, a Prefeitura de Caraguatatuba no havia retirado o golfinho da praia. +O governo vai usar a URV para corrigir impostos, garantiu FHC. +A URV produzida por trs ndices, e um dos ndices o mesmo da Ufir. +Vai ter paridade, j desde o comeo, explicou. +Quanto s tarifas pblicas, no sero fixadas em URV para evitar reajustes dirios, mas tero correes peridicas pelo indexador. +Do ponto de vista de proteo ao consumidor, vou reajustar o preo da tarifa pblica no dia normal como se ela fosse URV. +Ou seja, pela mdia real dos ltimos quatro meses, disse. +Onze milhes de aposentados ganham mnimo. +Esses vo ter um aumento real, pelo que disse o ministro Cutolo (Srgio, da Previdncia), na passagem para a URV, de 17%. +E alguns vo ter de 30%. +Ele atribuiu a discusso sobre o valor dos benefcios a uma iluso monetria. +As pessoas pensam que recebem US$ 80, mas 30 dias depois esse valor muito menor. +Encerrada a festa, Maluly viu o rapaz esperando. +O deputado se aproximou. +Eu precisava de uma ajuda do senhor, deputado ... +O Prncipe Charles, do Reino Unido, fez respirao boca-a-boca num boneco (foto) na inaugurao do novo prdio do Centro de Ambulncias de Wellington, capital da Nova Zelndia. +O homem que atacou Charles anteontem no ter direito a fiana e ficar preso pelo menos at o fim da visita de cinco dias do prncipe ao pas. +Litunia e Ucrnia elogiam a Otan Os presidentes da Litunia, Algirdas Zebrauskas, e da Ucrnia, Leonid Kravtchuk, elogiaram ontem o programa de Parceria pela Paz da Otan (aliana militar ocidental, liderada pelos EUA), que prev operaes conjuntas com pases do Leste Europeu. +Segundo eles, os laos com a aliana no vo afetar as relaes com a Rssia. +No verdade, como diz Gusmo, que a misso enviada a Israel tenha feito confronto de preos. +O relatrio sigiloso no traz qualquer lista de preos. +As evidncias de superfaturamento foram levantadas pelos professores Armando Lagan e Mrcio Rillo, da USP, a pedido da Folha. +Sustentadas em depoimentos PF, foram confirmadas em trs percias. +A ltima delas, com a participao de trs cientistas, comprovou superfaturamento de 343%. +Stephen Freehill, 16 (foto), compareceu ontem a um tribunal de Cingapura para uma audincia preliminar. +Ele acusado de vandalismo. +Pode ser condenado ao aoite, como seu compatriota Michael Fay, 18, preso na mesma ocasio. +Fay foi punido com quatro vergastadas e agora cumpre pena de priso. +Foras do norte do Imen anunciaram a tomada da base area de Al Anad, a principal dos sul-iemenitas. +A base fica a apenas 50 km de den, a capital do sul. +A guerra comeou h 13 dias por causa de um confronto entre o presidente Ali Abdulllah Saleh e seu vice. +No damos conta de atend-los. +A resposta ao pedido de ajuda foi gigantesca, diz Virginia de la Guardia, porta-voz da Mdicos Sem Fronteiras. +A maioria dos voluntrios so jovens. +Mas, comenta Enrique Albizu, presidente da Medicus Mundi, precisamente nas situaes de emergncia que necessitamos de gente mais especializada. +O Santos volta a viver um clima tenso para o prximo jogo, domingo contra o Santo Andr, em busca da primeira vitria no Campeonato Paulista. +O tcnico Pepe, irritado com a atuao do time no empate em 1 a 1 com Ituano anteontem, disse que est faltando tranquilidade e talento aos jogadores. +Pepe ainda tem esperana que o centroavante Guga renove o seu contrato, vencido em 31 de dezembro. +Se isso acontecer, tem que ser rpido, do contrrio no adianta mais, afirmou. +Nascido no dia 23 de maio de 1972, em So Paulo, Rubens Gonalves Barrichello comeou sua carreira automobilstica no kart, em 81, quando foi vice-campeo jnior da capital paulista. +Em 1989, aps ganhar diversos ttulos paulistas e brasileiros no kart, Barrichello disputou o Campeonato Brasileiro de Frmula Ford, terminando em quarto lugar. +A Sunab (Superintendncia Nacional de Abastecimento) autuou sbado duas lojas do ParK Shopping, em Braslia. +Ambas no apresentavam preos de produtos expostos nas vitrines. +A multa para lojistas que descumprirem a determinao pode chegar a R$ 128 mil. +Segundo Eduardo Lago, superintendente do orgo, j foram aplicadas 400 multas em todo o pas desde o incio do ms. +A Mercedes-Benz deve instalar a linha de montagem do Swatch, carro compacto com design do fabricante dos relgios da marca, em Sarreguemines, cidade do norte da Frana. +A deciso foi tomada pelo conselho da montadora alem, na ltima sexta-feira. +O anncio oficial ser feito aps o dia 20. +Beckenbauer aconselha o lateral a conversar com o treinador, procurando reverter a situao. +O caso Effenberg, jogador cortado por ter feito gesto obsceno torcida, tambm foi criticado. +Foi um ato muito rigoroso. +Beckenbauer tem o direito de dizer o que quiser. +Rodado no incio da dcada de 50, poca urea do macarthismo perseguio a artistas e intelectuais comunistas ou acusados de simpatizar com essa ideologia liderada pelo senador Joseph McCarthy nos EUA, o filme ganha verso para CD-ROM, com direito a um extenso material de referncia. +Preo: US$ 74,71. +O filme narra uma greve de mineiros em uma cidade do Novo Mxico, nos EUA. +H uma trama paralela, com Ramon e Esperanza, casal que aps a greve se separa. +Caso isso ocorresse, eles seriam colocados em liberdade to logo chegassem no Canad. +Documento do Ministrio das Relaes Exteriores do Canad, obtido pela Folha, d apoio ao lobby (grupo de presso) feito pela Embaixada do Canad junto a senadores e deputados brasileiros para que os canadenses sejam expulsos. +Segundo o cnsul, era crescente a presso do Congresso norte-americano sobre o Departamento de Estado para que se adotasse uma poltica idntica do Brasil. +Queremos dar um visto por dez anos. + mais eficiente do que por apenas quatro anos. +Estamos sendo forados, ao invs, a reduzir o prazo para trs meses. + uma situao absurda, disse Taylor, 49. +Um jogador que estava nos planos de Pepe era o meia Carlos Alberto Dias, que acabou indo para o Flamengo, j que a diretoria do Santos vetou o seu nome. +No momento, Pel, o maior jogador da histria da equipe, tambm a maior esperana de um time forte. +Durante esta semana, o agora dirigente esteve no Japo, em busca de uma empresa que, no futuro, invista no Santos. +Recentemente, o time firmou um contrato de US$ 50 mil mensais com a Lousano, que vale at o fim do Campeonato Paulista. +O aumento das despesas acontece at mesmo nos ministrios da Fazenda e do Planejamento, responsveis pela montagem do Oramento federal. +No Ministrio da Fazenda, as despesas crescero de US$ 1,13 bilhes em 93 para US$ 1,36 bilhes em 94 (ou mais 20,07%). +No Planejamento, o crescimento de 33,19%, elevando as despesas de US$ 87,6 milhes em 93 para US$ 116,69 milhes em 94. +O deputado Aloizio Mercadante (PT/SP) divulgou ontem nmeros do Tesouro sobre a execuo oramentria de janeiro a novembro de 1993, demonstrando que o governo conseguiu um supervit operacional de US$ 9,2 bilhes neste perodo. +Embora esteja a quase 40 km do estdio, o hotel foi escolhido porque est prximo da California State University, onde o Brasil treinar durante esta semana. +A chegada ao hotel foi s 12h10 (16h10 em Braslia). +Toda a delegao subiu diretamente para os quartos. +Os jogadores e Parreira no falaram com a imprensa. +Para estimular a participao, a cada dois dias de trabalho as meninas ganham uma boneca. +A Repblica Movimento de Emas trabalha h 20 anos com adolescentes carentes. +Cheia de energia, a interpretao da Orquestra Estadual da Hungria vem amortecida um pouco pela artificialidade da gravao, de quase 15 anos atrs, mas compensa a sonoridade de estdio com o vigor. +A Sinfonia toda uma espcie de demonizao da Nona de Beethoven, que muito mais seu assunto do que o Fausto de Goethe. +Nisto tambm na resistncia e repetio de Beethoven Liszt um foco importante, com reflexos na obra de Tchaikovsky, Scriabin e Strauss. +No poderia haver realizao maior para um compositor: substituir Beethoven, ou ter pelo menos a iluso de ser um novo ponto de partida. +Para Simon Franco, 55, presidente da Simon Franco Recursos Humanos, no se pode jogar fora 15 anos de experincia. +Segundo ele, Carvalho no est avaliando que pode ampliar seu espectro de opes sem a necessidade de uma mudana radical. +A reproduo assistida est regulamentada no Brasil desde novembro de 1992, embora sua prtica tenha sido iniciada oito anos antes. +Determinadas pelo Conselho Federal de Medicina (CFM) as normas ticas foram inspiradas nas de outros pases Estados Unidos, Frana e Itlia, por exemplo. +Mas, como nesses pases, os avanos nas pesquisas mdicas tornaram os cdigos de tica ultrapassados. +A possibilidade da gravidez ps-menopausa e o uso de vulos de fetos abortados (veja texto pg. 6) so exemplos mais gritantes. +Ficaram de fora de nossa regulamentao, disse Folha Antonio Henrique Pedrosa Neto, diretor do CFM. +Ele afirmou, no entanto, que pelo menos a gravidez ps-menopausa ser objeto de nova resoluo. +Polcia Civil encontrou dois homens amarrados, amordaados e de olhos vendados, em um barraco da favela Nova Braslia (zona norte). +Mauro Pereira e Gilson Paulino dos Santos disseram ter sido sequestrados por traficantes, que os teriam confundido com policiais. +Fiscais do Tribunal Regional Eleitoral e PMs do Batalho de Choque foram recebidos a tiros ontem tarde na Vila do Joo, Manguinhos (zona norte do Rio). +Os fiscais retirar propaganda irregular. +No tiroteio, morreu Adriano Herculano da Silva, 18. +O casamento ser realizado pelo reverendo Onaldo Pereira, 38, ordenado h oito anos nos Estados Unidos. +O pastor responsvel no Brasil pela Comunidade Pacifista Crist, fundada na Alemanha em 1708. +A seleo brasileira feminina de basquete a campe do Torneio Internacional de Basquete, um quadrangular amistoso disputado em Recife (PE) e que contou com a participao das selees de Cuba, Argentina e Eslovnia. +O time comandado pelo tcnico Miguel ngelo da Luz ficou com o ttulo ao derrotar anteontem noite na final a forte seleo de Cuba por 115 a 93 (54 a 44 no primeiro tempo). +A cestinha do jogo foi Hortncia, que marcou 38 pontos. +Alm da fazenda do Sabi, participaram do leilo a Terra Boa e a Mata Velha, tradicionais criadoras de nelore. +O principal destaque na pista foi a vaca Santya, selecionada para ser doadora em transferncia de embries, que foi vendida por 40,2 mil URVs. +Filha do touro Chummak, ela est prenha e foi comercializada com uma bezerra ao p. +A Times Square tambm um ponto de encontro e uma alternativa teen, afirma o diretor da Multiplan. +Para Spinelli, o espao representa uma nova tendncia para as reas de convenincia dos shoppings. +Paula Toller Isso uma coisa que a gravadora vinha sugerindo para a gente h muito tempo. +Gostamos da idia, resolvemos pegar velhos sucessos e fazer neste formato. +Gravaes acsticas se encaixam com o nosso tipo de som. +Sempre tivemos canes mais lentas. +Bruno Fortunato A gente sempre teve msicas que do para tocar no violo. +A inteno louvvel e esta Folha tem denunciado sempre a ciranda financeira como obstculo a uma autntica estabilizao. +O momento e a forma escolhidos para mudar o sistema financeiro, entretanto, dependem fortemente de variveis polticas. +Est em estudo no Ministrio da Previdncia um conjunto de medidas para desestimular o aumento de aposentadorias no prximo ano. +Elas sero oferecidas a Itamar, como alternativa propalada quebra do caixa previdencirio. +Causou m impresso, na quarta, a participao do ministro da Justia, Alexandre Dupeyrat, na reunio que tratou do projeto sobre abuso do poder econmico. +Chegou atrasado, foi duro com seus crticos e saiu antes do fim. +Em Olhos de Serpente (Snake Eyes) de Abel Ferrara, Madonna faz o papel dela mesma: uma falsa atriz, falsa loira, que paga para estar num filme. + de longe sua melhor atuao no cinema. +Ferrara pe o excelente Harvey Keitel para interpretar o seu papel: o do diretor que faz da falsa loira uma verdadeira atriz. +Olhos de Serpente o filme dentro do filme dentro do filme. +Eddie (Harvey Keitel) dirige a histria de um casal de ricos drogados em crise (Madonna e James Russo). +Eddie tambm est em crise com sua mulher (interpretada pela mulher de Abel Ferrara) e no resiste tentao da atriz, que paga para ele filmar (Madonna produtora de Olhos de Serpente). +Caso o tetracampeo seja o Brasil (seja feita a vossa vontade assim na Terra como nos gramados), confirma-se que os ltimos sero os primeiros. +Vi, num jogo quase inacreditvel da Copa, o Eire jogar futebol muito melhor do que a Itlia. +Venceu por 1 a 0. +J bastava, para orgulho da Irlanda, que a Inglaterra sequer tivesse chegado Copa. +Mas na Republica Dominicana que o cruzeiro atinge seu ponto alto. +O navio ancora numa praia, Serena Cay, que se torna exclusiva dos 1.600 passageiros. +Na areia h apenas espreguiadeiras, coqueiros e barraquinhas de artesanato. +O mar cristalino. +Quem quiser, vai em excurso at o resort local, Casa de Campo. +Sem dizer que o seu clube dispensou, humilhantemente, o seu arqueiro Zubizarreta -- depois da derrota para o Milan, 0 a 4, na deciso da Copa da Europa. +Chegamos at aqui, claro, tambm graas a Cruyff. +S Salinas discordou dessa posio. +Dos dez cestinhas da atual temporada, apenas um joga fora do garrafo. +Dos cinco primeiros artilheiros, quatro so superpivs. +Todos eles estaro esta noite em Minneapolis. +Chance imperdvel para aqueles que apreciam enterradas, tocos e a mais primitiva troca de porradas. +A NBA nem existia e o profissionalismo no esporte apenas engatinhava quando apareceu o primeiro gigante do basquete norte-americano. +Seu nome: George Mikan. +Sua virtude: 2,08 m. +Em torno do gabinete presidencial no Planalto ele quer os dois maiores colaboradores na campanha eleitoral: o amigo, empresrio e secretrio-geral do PSDB Srgio Motta e o ex-reitor da Unicamp e coordenador do programa de governo Paulo Renato de Souza. +Cardoso diz que a frente para sua eleio foi formada exclusivamente em torno de um programa. +O nmero mais alarmante se pensarmos que daria para abastecer toda a populao brasileira por oito meses, diz Dalmo Rosalen, da Secretaria do Meio Ambiente. +H cidades que enfrentam programas de racionamento de gua. +Um exemplo a bacia do Vale do Piracicaba (a 170 quilmetros a noroeste de So Paulo). +Flach Hoje esto estabelecidas penas de priso, mesmo que eventualmente no sejam cumpridas ou sejam convertidas em prestao de servios comunidade. +Mas eu imagino que deveria se dotar, pelo menos o Judicirio, de medidas que possibilitassem a concesso do perdo judicial. +Folha -- O senhor acha que o usurio questo de sade, de educao ou de polcia? +Flach No terreno das idias, eu priorizo como uma questo de educao e de sade. +Eu estou muito preocupado em estimular as atividades preventivas. +Nossa preocupao um projeto que estimule no jovem uma vida sem drogas, promovendo-se um comportamento construtivo. +A espanhola Arantxa Sanchez, tenista com mlehor desempenho nesta temporada, foi eliminada na primeira rodad do Virginia Slims Masters em Nova York, que rene as 16 melhores jogadoras do ano. +Sanchez, n 2 do ranking mundial, perdeu num jogo dramtico para a francesa Julie Halard por 6/2, 1/6 e 7/6, com 7/2 no tie-break decisivo. +Na 22 posio do ranking, Halard foi empurrada pelo pblico no Madison Square Garden. +Se for uma greve s por grevismo, da prejudica a quem favorvel a isso. +A, poderia afetar o Lula, no a mim, disse FHC. +O candidato tucano a presidente deu essa declarao ontem tarde, depois de discursar para cerca de 550 empresrios na hora do almoo (leia texto nesta pgina). +A PMD (psicose manaco-depressiva) uma doena psiquitrica que leva a uma alterao abrupta do comportamento. +As pessoas com PMD oscilam entre dois plos. +Tm fases de depresso e de euforia (mania). +Nem todos passam pelas duas fases. +Os irmos Osny Silveira Neto e Guilherme Silveira abrem hoje o bar Horcio no Itaim. +Jos Arthur Giannotti, Marilena Chaui, Gloria Kalil e Jorge da Cunha Lima faziam parte de platia-cabea que acompanhou anteontem no Masp a palestra de Claude Lefort apresentado por Srgio Cardoso. +Essa poca marca a formao da base da moral samurai, resultado da disciplina fsica e mental do zen-budismo, dos ditames do confuncionismo e do esprito militarista reinante. +Os EUA entram na vida japonesa em meados do sculo 19, quando obrigam o shogun a assinar um tratado de comrcio. +Esse sinal de fraqueza associado a crises internas levam queda do sistema em 1867. +Um ano depois, a capital passa a ser Tquio. +A hidroginstica uma alternativa para quem no tem acompanhante. +A professora Cludia Morgado, 25, da academia Competition, recomenda exerccios leves, como a corrida. +O importante levantar bem o joelho, manter o ritmo e encostar o calcanhar no cho a cada movimento, diz. +Primeiro foi uma oportunidade de esclarecer muitos pontos que eu no conhecia antes. +Mesmo entendendo suas explicaes sobre o programa de governo, entendo que ele tinha que ter um programa de governo, que vai dar um meio de cobrar depois. +Um programa um planejamento para se atingir certas metas. +A falta de um programa poderia dar a impresso de no ter metas. +Gostei da sua posio sobre a religio. +Mesmo criticado, ele defende sua posio claramente em favor dos evanglicos e de seu ponto de vista religioso. +Em muitos momentos ele simplesmente deixou de se posicionar. +Talvez ele no tenha idias elaboradas sobre os problemas do Estado. +Ele foi mais direto, mais atacante, deu respostas mais profundas e melhores. +Minha expectativa no foi atendida em vrios pontos. +Redes, multimdia, sistemas operacionais ou informtica na educao. +Esses so alguns dos temas que estaro sendo discutidos no Congresso da Fenasoft, promovido paralelamente feira. +O congresso dividido em trs temas, com o objetivo de atender de profissionais de informtica a executivos e empresrios interessados em novas tecnologias, pasando pelo usurio final. +O tcnico da Holanda, Dick Advocaat, disse que sua preocupao anular o esquema defensivo da Irlanda. +Ele afirmou que pretende reforar seu meio-campo, mas no adiantou a escalao da equipe. +Folha -- O senhor foi indiciado em inqurito da Polcia Federal por manipulao de arbitragem, estelionato e formao de quadrilha. +O senhor teme ser condenado? +Miranda -- Em relao a mim isso no vai dar absolutamente em nada. +No sei em relao a os outros indiciados. +Me indiciaram precipitadamente, aodadamente. + coisa orquestrada, dirigida. +Com pregaes que duram at dois minutos. +Os candidatos da Bahia esto encontrando certa dificuldade em contratar cantores para animar suas campanhas. +O maior aumento aconteceu em direito (mais 366 candidatos) e a maior queda, em engenharia (menos 980). +O curso que mais cresceu foi odontologia na USP em Bauru 83,5%. +O nmero total de candidatos no vestibular caiu em relao ao ano passado. +De 140.518 inscritos, a Fuvest registrou 139.369. +A inteno de Fleury vender as aes para equilibrar as finanas e comear a pagar a dvida do Estado, estimada em R$ 31 bilhes. +Segundo a assessoria de imprensa do governador, Fleury j pediu um levantamento de preo das aes, junto a consultores especializados, para estabelecer o valor mnimo das aes, que vo ser levadas a leilo. +O ministro da Fazenda, Rubens Ricupero, disse que o resultado do IPC-r no uma surpresa. +Ele explicou que o ndice foi calculado do dia 16 de junho (ainda sobre cruzeiros reais) ao dia 14 deste ms. +O IPC-r de julho, segundo o ministro, tem muito pouco da nova moeda. +Normalmente ns utilizamos dados histricos sobre a produtividade em cada regio, alm de informaes de agricultores, diz. +Segundo Formaggio, esto em estudo outras formas de obter a produtividade das culturas. +Ele estima em dez horas o tempo necessrio para classificar as reas e culturas plantadas em uma extenso de 180 quilmetros. +O prefeito de Juazeiro, Manoel Salviano (PSDB), disse que qualquer funcionrio que tenha participado da recepo foi espontaneamente, sem nenhuma presso da prefeitura. +Acompanhada de Renata Queirz, mulher do candidato do PSDB ao governo do Cear, Tasso Jereissati, e de Patrcia Gomes, mulher do ministro da Fazenda, Ciro Gomes, Ruth Cardoso visitou trs cidades cearenses antes de Juazeiro. +Entre as mais de 140 participantes da feira, estavam presentes companhias como a Xerox, Kodak, QMS, Digital e Pennant, empresa da IBM para a rea de impresso. +A Kodak, por exemplo, apresentou as impressoras/copiadoras ColorEdge 1.560 e 1.565, capazes de copiar em cores frente e verso de documentos de vrias pginas. +Podem produzir pequenas brochuras, mala direta, folhetos, realizando at sete impresses por minuto. +Kim E sobre como eu fiquei totalmente nua o tempo todo. +Assista ao filme de novo: voc no vai me ver completamente nua a no ser numa cena, no final do strip tease. +E por trs ... +Est tudo na sua cabea. +Como quando eu conheci Alec. +Jurava que ele usava um chapu de cowboy. +Alec Nunca usei um chapu daqueles na vida! +Nasci em Lono Island. +O que um chapu de cowboy estaria fazendo ali? +Apesar de haver anunciado que o treino da Bulgria seria aberto ao pblico, os dirigentes mudaram de idia pouco antes do incio do treinamento e proibiram a presena de torcedores no campo da Southern Methodist University, em Dallas. +Os dirigentes da seleo pediram aos seguranas da universidade que afastassem o pblico, composto em sua maior parte por jovens, que j se preparava para acompanhar os treinos. + noite, o projeto prev aulas de alfabetizao para jovens de 14 a 22 anos nos Cieps. +Na eleio de 89, Leonel Brizola prometia a construo de 10 mil Cieps. +No projeto no h definio de quantos podem ser construdos. +O custo mdio de cada um dos Cieps no Rio de Janeiro foi de cerca de US$ 1 milho. +Nossa preocupao promover o desenvolvimento. +E como no h crescimento sem educao, a causa das causas para ns a elevao do nvel educacional do povo brasileiro. +Claro sem descuidar de tudo mais, mas vendo com clareza a questo das prioridades, declara Brizola. +Em Manaus, o representante da famlia Graa Manuel Tavares da Graa, candidato derrotado a deputado estadual pelo PP. +Paixo irmo de Jos Tavares da Graa, preso em agosto de 93 em Belm (PA) com 435 quilos de cocana. + tambm primo de Curica e de Floriano Graa. +Fui procurado pelos ladres para comprar a carga. +Fomos eu, o Violim, e os investigadores Gilberto Brito, Euripedes Tozzo, Mauro, Marco e o Adner. +Prenderam todos e apreenderam a mercadoria. +Eles desviaram quatro toneladas da carga. +Quatro Kombis foram para o Deic. +S isso foi devolvido para a Riachuelo. +No realismo moderno de John Cassavettes (1929-89), por exemplo, o ator construa o personagem em tempo real diante da cmera, baseado no improviso. +Cada hesitao, cada silncio, cada desvio do olhar era um ganho na contabilidade do sublime que o diretor-autor planejava. +Na semana passada, o jogador Buchwald criticou a escalao de Matthaeus como lbero, pedindo que ele jogasse no meio-campo. +O tcnico da Coria, Kim Ho, afirmou que, no primeiro tempo, a equipe estava intimidada pela Alemanha, o que natural, pois eles so campees do mundo. +O secretrio-geral do Conselho de Segurana da ONU, Boutros Boutros-Ghali, pediu que as potncias que negociam um tratado de paz com as faces em luta na Bsnia formem uma fora-tarefa para agir na regio. +Boutros-Ghali teme que as potncias se retirem e suspendam sanes contra a Srvia pondo em risco outras partes da ex-Iugoslvia se o plano for aceito. +Folha -- Qual o segredo da vitria? +Gilbert -- Acho que Andre usou essa mesma presso a seu favor. +Ele funciona desse jeito. +Se voc bate nele, o revide vem forte, pode apostar. +Alguns tenistas so assim, usam sua carga emocional como combustvel. +Haver mais e melhores escolas, laboratrios, universidades e bolsas de estudo. +Pequenas e mdias empresas, Foras Armadas, deficientes fsicos e artistas tero apoio e recursos. +Segundo FHC, o Brasil possui esses recursos, muitos de eles naturais, e vantagens comparativas para realizar essas metas. +Segundo ele, o tubo era independente do sistema de propulso do submarino e o vapor que escapou no radiativo. +O Emeraude no carrega armamento nuclear. +A energia nuclear usada s para movimentar a embarcao. +Entre cada parte do submarino h isolamento, evitando que acidentes numa parte atinjam as outras. +O relaes pblicas da corporao, tenente-coronel Fernando Belo, disse que o encontro foi uma oportunidade para agradecer e elogiar a colaborao da PM. +Belo negou que na reunio tenha sido discutida a incorporao aos quadros da PM de cerca de 2.000 soldados do Exrcito. +A possibilidade foi levantada em encontro do governador eleito, Marcello Alencar (PSDB), com oficiais do CML. +As emissoras se negaram a se retratar e o juiz ameaou terminar com a cobertura do julgamento, sem especificar como. +Ito pode anunciar hoje que a transmisso est proibida. +A lei do Estado da Califrnia d ao juiz de cada caso autoridade para impedir a presena de cmaras no tribunal se achar que ela prejudica a Justia. +ANGOLA, UMA TRAGDIA ESQUECIDA -- O fotgrafo Andr Penner expe 29 fotos coloridas que retratam o cotidiano da guerra civil em Angola. +Abertura hoje s 19h30. +Seg a sex das 9h30 s 18h30. +Preo das fotos: US$ 300. +At 17 de junho. +BASTIDORES DOS MUSEUS -- O trabalho feito pelos fotgrafos Eduardo Castanho, Saul Queiroz, Rogrio Voltan, Ricardo Hantzchel, Fausto Chermont e Eliana Lopes retrata o que as pessoas no vem ao visitar um museu. +Na Anhanguera, que une So Paulo a Ribeiro Preto, costuma haver neblina pesada entre o km 21 e o km 43, tambm perto de Perus e Cajamar. +Isto ocorre principalmente noite e a visibilidade pode chegar a zero. +A Rodovia Castelo Branco, que segue para o oeste do Estado, apresenta vrios pontos com neblina. +O mais perigoso na regio da Serra das Conchas, entre o km 129 e o km 162, logo aps Tatu, para quem vai para o interior. +O paulistano no acredita em queda de inflao nos prximos meses. +O ndice deve aumentar, segundo 45% dos entrevistados pelo DataFolha. +A impresso de que o ndice de alta dos preos cai se restringe a 27% das respostas. +Tudo fica como est na opinio de 23%, enquanto 5% no sabem responder. +A derrota (1 a 0) para a fraca seleo dos Estados Unidos conturbou o ambiente da seleo mexicana. +Um grupo de torcedores arremessou ovos podres e insultos contra o time, na volta para a Cidade do Mxico na noite de domingo passado. +Nascidos para perder. +Segundo a agncia de noticias Notimex, a reao pode ser uma amostra de que como vai reagir a torcida se o Mxico no se classificar para a segunda fase da Copa. +Adoraria ser candidata. +Tenho certeza de que se pudesse concorrer a algum cargo, me elegeria. +Eu adoro poltica, confessa Mercedes Rossi de Almeida, 73, a me do candidato ao governo de So Paulo Francisco Rossi (PDT). +O painel vai funcionar das 8h s 22h. +O pblico alvo so os trs milhes de pedestres que circulam diariamente na regio. +Empresa funciona das 9h s 19h, diariamente. +Na Samp, dois titulares esto fora de combate: o zagueiro Marco Rossi e o armador srvio Jugovic. +Pior, o mister sueco Sven-Goran Eriksson tambm no poder contar com o garoto Bertarelli, contratura na coxa direita. +Retorna ao banco o bom volante eslovnio Katanec. +E, depois de uma semana com gripe, recupera a sua forma o holands Ruud Gullit, artilheiro do time com 14 tentos. +Na terceira colocao, 34 pontos, a Juventus de Turim visita o Genoa, 22, no limiar do rebaixamento. +A Juve vive a transio para uma nova temporada. +Acabou-se a gesto do ex-craque Giampiero Boniperti na administrao do clube. +Em seu lugar, vai assumindo um outro ex-atleta, Roberto Bettega. +Com Boniperti se despede o tcnico Giovanni Trapattoni, que ceder o posto a Marcello Lippi, hoje no Napoli. +A torcida da Senhora se conforta com a melhora de Roberto Baggio, que no ter de operar o joelho. +O projeto vem sendo implementado, em carter experimental, em trs escolas: Morumbi, Visconde de Porto Seguro (particulares) e Godofredo Furtado (estadual). +O programa ter ao integrada com pais e alunos atravs da formao de grupos de informao, diz Antonio Carlos Gomes da Silva, superintendente do HC. +4) A realizao no Cairo (Egito) da Conferncia Internacional sobre Populao e Desenvolvimento da ONU tem como maior objetivo a divulgao mundial de polticas para o controle de natalidade como forma de reduo da pobreza. +A populao mundial est em torno de 5,66 bilhes. +VERA LCIA DA COSTA ANTUNES coordenadora de geografia do curso e colgio Objetivo. +Com a determinao da Justia, os empregados ficam impedidos de fazer o pagamento das aes. +O chefe da gerncia jurdica do BNDES, em So Paulo, Arnaldo Montenegro, 43 disse que ser impetrada uma ao de agravo de instrumento com pedido de reconsiderao ao juiz Andrade Martins. +O banco no informou porque o sigilo no foi mantido tambm para os grupos estrangeiros. +A relao entre os dois piorou quando Fishel arranjou uma amante, no final dos anos 80. +Ele teria deixado a mulher e trazido a amante para Roma. +Vrias vezes os dois foram pegos fazendo amor na minha clnica. +Eu tolerei pois no quis me intrometer na vida privada dos meus funcionrios, disse Antinori, para quem Fishel era um excelente tcnico. +Era um miservel quando chegou. +Em poucos anos ganhou mais de US$ 1 milho. +Aps vrias divergncias com Fishel, Antinori o demitiu em 1990. +Nessa poca, Fishel teria conhecido o ginecologista britnico Robert Winston, a quem convidou para um encontro cientfico promovido por Antinori. +O cineasta italiano Franco Zefirelli revelou que, para iludir o servio militar durante o perodo fascista, se tornou um partisan e acabou matando um soldado alemo. +As sinalizaes que tem dado o presidente eleito, Fernando Henrique Cardoso, sobre a reforma patrimonial do Estado so auspiciosas. +Mas tambm preciso a sociedade mobilizar-se para sensibilizar o futuro Congresso Nacional na promoo de reformas constitucionais indispensveis ao xito das mudanas pretendidas. +O patrimnio pblico no fetiche a ser adorado e sim bem a ser utilizado principalmente em favor de quem no tem acesso a escolas, nem assistncia mdica, nem a empregos. +Abram Szajman, 58, presidente da Federao do Comrcio do Estado de So Paulo e do Conselho Deliberatico do Sebrae-SP (Servio Brasileiro de Apoio s Micro e Pequenas Empresas). +Alm de Mauro Salles que surpreendeu a galera ao revelar seu ct de fotgrafo profissional no nico da carreira, o foco principal do encontro foi a mulher de J.R.Duran, Alexandra Brochen. diff --git a/nltk/test/framenet.doctest b/nltk/test/framenet.doctest new file mode 100644 index 0000000..9c93d57 --- /dev/null +++ b/nltk/test/framenet.doctest @@ -0,0 +1,288 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +======== +FrameNet +======== + +The FrameNet corpus is a lexical database of English that is both human- +and machine-readable, based on annotating examples of how words are used +in actual texts. FrameNet is based on a theory of meaning called Frame +Semantics, deriving from the work of Charles J. Fillmore and colleagues. +The basic idea is straightforward: that the meanings of most words can +best be understood on the basis of a semantic frame: a description of a +type of event, relation, or entity and the participants in it. For +example, the concept of cooking typically involves a person doing the +cooking (Cook), the food that is to be cooked (Food), something to hold +the food while cooking (Container) and a source of heat +(Heating_instrument). In the FrameNet project, this is represented as a +frame called Apply_heat, and the Cook, Food, Heating_instrument and +Container are called frame elements (FEs). Words that evoke this frame, +such as fry, bake, boil, and broil, are called lexical units (LUs) of +the Apply_heat frame. The job of FrameNet is to define the frames +and to annotate sentences to show how the FEs fit syntactically around +the word that evokes the frame. + +------ +Frames +------ + +A Frame is a script-like conceptual structure that describes a +particular type of situation, object, or event along with the +participants and props that are needed for that Frame. For +example, the "Apply_heat" frame describes a common situation +involving a Cook, some Food, and a Heating_Instrument, and is +evoked by words such as bake, blanch, boil, broil, brown, +simmer, steam, etc. + +We call the roles of a Frame "frame elements" (FEs) and the +frame-evoking words are called "lexical units" (LUs). + +FrameNet includes relations between Frames. Several types of +relations are defined, of which the most important are: + +- Inheritance: An IS-A relation. The child frame is a subtype + of the parent frame, and each FE in the parent is bound to + a corresponding FE in the child. An example is the + "Revenge" frame which inherits from the + "Rewards_and_punishments" frame. + +- Using: The child frame presupposes the parent frame as + background, e.g the "Speed" frame "uses" (or presupposes) + the "Motion" frame; however, not all parent FEs need to be + bound to child FEs. + +- Subframe: The child frame is a subevent of a complex event + represented by the parent, e.g. the "Criminal_process" frame + has subframes of "Arrest", "Arraignment", "Trial", and + "Sentencing". + +- Perspective_on: The child frame provides a particular + perspective on an un-perspectivized parent frame. A pair of + examples consists of the "Hiring" and "Get_a_job" frames, + which perspectivize the "Employment_start" frame from the + Employer's and the Employee's point of view, respectively. + +To get a list of all of the Frames in FrameNet, you can use the +`frames()` function. If you supply a regular expression pattern to the +`frames()` function, you will get a list of all Frames whose names match +that pattern: + + >>> from pprint import pprint + >>> from operator import itemgetter + >>> from nltk.corpus import framenet as fn + >>> from nltk.corpus.reader.framenet import PrettyList + >>> x = fn.frames(r'(?i)crim') + >>> x.sort(key=itemgetter('ID')) + >>> x + [, , ...] + >>> PrettyList(sorted(x, key=itemgetter('ID'))) + [, , ...] + +To get the details of a particular Frame, you can use the `frame()` +function passing in the frame number: + + >>> from pprint import pprint + >>> from nltk.corpus import framenet as fn + >>> f = fn.frame(202) + >>> f.ID + 202 + >>> f.name + 'Arrest' + >>> f.definition + "Authorities charge a Suspect, who is under suspicion of having committed a crime..." + >>> len(f.lexUnit) + 11 + >>> pprint(sorted([x for x in f.FE])) + ['Authorities', + 'Charges', + 'Co-participant', + 'Manner', + 'Means', + 'Offense', + 'Place', + 'Purpose', + 'Source_of_legal_authority', + 'Suspect', + 'Time', + 'Type'] + >>> pprint(f.frameRelations) + [ Child=Arrest>, Component=Arrest>, ...] + +The `frame()` function shown above returns a dict object containing +detailed information about the Frame. See the documentation on the +`frame()` function for the specifics. + +You can also search for Frames by their Lexical Units (LUs). The +`frames_by_lemma()` function returns a list of all frames that contain +LUs in which the 'name' attribute of the LU matches the given regular +expression. Note that LU names are composed of "lemma.POS", where the +"lemma" part can be made up of either a single lexeme (e.g. 'run') or +multiple lexemes (e.g. 'a little') (see below). + + >>> PrettyList(sorted(fn.frames_by_lemma(r'(?i)a little'), key=itemgetter('ID'))) + [, ] + +------------- +Lexical Units +------------- + +A lexical unit (LU) is a pairing of a word with a meaning. For +example, the "Apply_heat" Frame describes a common situation +involving a Cook, some Food, and a Heating Instrument, and is +_evoked_ by words such as bake, blanch, boil, broil, brown, +simmer, steam, etc. These frame-evoking words are the LUs in the +Apply_heat frame. Each sense of a polysemous word is a different +LU. + +We have used the word "word" in talking about LUs. The reality +is actually rather complex. When we say that the word "bake" is +polysemous, we mean that the lemma "bake.v" (which has the +word-forms "bake", "bakes", "baked", and "baking") is linked to +three different frames: + +- Apply_heat: "Michelle baked the potatoes for 45 minutes." + +- Cooking_creation: "Michelle baked her mother a cake for her birthday." + +- Absorb_heat: "The potatoes have to bake for more than 30 minutes." + +These constitute three different LUs, with different +definitions. + +Multiword expressions such as "given name" and hyphenated words +like "shut-eye" can also be LUs. Idiomatic phrases such as +"middle of nowhere" and "give the slip (to)" are also defined as +LUs in the appropriate frames ("Isolated_places" and "Evading", +respectively), and their internal structure is not analyzed. + +Framenet provides multiple annotated examples of each sense of a +word (i.e. each LU). Moreover, the set of examples +(approximately 20 per LU) illustrates all of the combinatorial +possibilities of the lexical unit. + +Each LU is linked to a Frame, and hence to the other words which +evoke that Frame. This makes the FrameNet database similar to a +thesaurus, grouping together semantically similar words. + +In the simplest case, frame-evoking words are verbs such as +"fried" in: + + "Matilde fried the catfish in a heavy iron skillet." + +Sometimes event nouns may evoke a Frame. For example, +"reduction" evokes "Cause_change_of_scalar_position" in: + + "...the reduction of debt levels to $665 million from $2.6 billion." + +Adjectives may also evoke a Frame. For example, "asleep" may +evoke the "Sleep" frame as in: + + "They were asleep for hours." + +Many common nouns, such as artifacts like "hat" or "tower", +typically serve as dependents rather than clearly evoking their +own frames. + +Details for a specific lexical unit can be obtained using this class's +`lus()` function, which takes an optional regular expression +pattern that will be matched against the name of the lexical unit: + + >>> from pprint import pprint + >>> PrettyList(sorted(fn.lus(r'(?i)a little'), key=itemgetter('ID'))) + [, , ...] + +You can obtain detailed information on a particular LU by calling the +`lu()` function and passing in an LU's 'ID' number: + + >>> from pprint import pprint + >>> from nltk.corpus import framenet as fn + >>> fn.lu(256).name + 'foresee.v' + >>> fn.lu(256).definition + 'COD: be aware of beforehand; predict.' + >>> fn.lu(256).frame.name + 'Expectation' + >>> fn.lu(256).lexemes[0].name + 'foresee' + +Note that LU names take the form of a dotted string (e.g. "run.v" or "a +little.adv") in which a lemma precedes the "." and a part of speech +(POS) follows the dot. The lemma may be composed of a single lexeme +(e.g. "run") or of multiple lexemes (e.g. "a little"). The list of +POSs used in the LUs is: + +v - verb +n - noun +a - adjective +adv - adverb +prep - preposition +num - numbers +intj - interjection +art - article +c - conjunction +scon - subordinating conjunction + +For more detailed information about the info that is contained in the +dict that is returned by the `lu()` function, see the documentation on +the `lu()` function. + +------------------- +Annotated Documents +------------------- + +The FrameNet corpus contains a small set of annotated documents. A list +of these documents can be obtained by calling the `docs()` function: + + >>> from pprint import pprint + >>> from nltk.corpus import framenet as fn + >>> d = fn.docs('BellRinging')[0] + >>> d.corpname + 'PropBank' + >>> d.sentence[49] + full-text sentence (...) in BellRinging: + + + [POS] 17 tags + + [POS_tagset] PENN + + [text] + [annotationSet] + + `` I live in hopes that the ringers themselves will be drawn into + ***** ******* ***** + Desir Cause_t Cause + [1] [3] [2] + + that fuller life . + ****** + Comple + [4] + (Desir=Desiring, Cause_t=Cause_to_make_noise, Cause=Cause_motion, Comple=Completeness) + + + >>> d.sentence[49].annotationSet[1] + annotation set (...): + + [status] MANUAL + + [LU] (6605) hope.n in Desiring + + [frame] (366) Desiring + + [GF] 2 relations + + [PT] 2 phrases + + [text] + [Target] + [FE] + [Noun] + + `` I live in hopes that the ringers themselves will be drawn into + - ^^^^ ^^ ***** ---------------------------------------------- + E supp su Event + + that fuller life . + ----------------- + + (E=Experiencer, su=supp) + + diff --git a/nltk/test/generate.doctest b/nltk/test/generate.doctest new file mode 100644 index 0000000..e116d65 --- /dev/null +++ b/nltk/test/generate.doctest @@ -0,0 +1,66 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +=============================================== +Generating sentences from context-free grammars +=============================================== + +An example grammar: + + >>> from nltk.parse.generate import generate, demo_grammar + >>> from nltk import CFG + >>> grammar = CFG.fromstring(demo_grammar) + >>> print(grammar) + Grammar with 13 productions (start state = S) + S -> NP VP + NP -> Det N + PP -> P NP + VP -> 'slept' + VP -> 'saw' NP + VP -> 'walked' PP + Det -> 'the' + Det -> 'a' + N -> 'man' + N -> 'park' + N -> 'dog' + P -> 'in' + P -> 'with' + +The first 10 generated sentences: + + >>> for sentence in generate(grammar, n=10): + ... print(' '.join(sentence)) + the man slept + the man saw the man + the man saw the park + the man saw the dog + the man saw a man + the man saw a park + the man saw a dog + the man walked in the man + the man walked in the park + the man walked in the dog + +All sentences of max depth 4: + + >>> for sentence in generate(grammar, depth=4): + ... print(' '.join(sentence)) + the man slept + the park slept + the dog slept + a man slept + a park slept + a dog slept + +The number of sentences of different max depths: + + >>> len(list(generate(grammar, depth=3))) + 0 + >>> len(list(generate(grammar, depth=4))) + 6 + >>> len(list(generate(grammar, depth=5))) + 42 + >>> len(list(generate(grammar, depth=6))) + 114 + >>> len(list(generate(grammar))) + 114 diff --git a/nltk/test/gensim.doctest b/nltk/test/gensim.doctest new file mode 100644 index 0000000..ecef28b --- /dev/null +++ b/nltk/test/gensim.doctest @@ -0,0 +1,141 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +======================================= +Demonstrate word embedding using Gensim +======================================= + + >>> from nltk.test.gensim_fixt import setup_module + >>> setup_module() + +We demonstrate three functions: +- Train the word embeddings using brown corpus; +- Load the pre-trained model and perform simple tasks; and +- Pruning the pre-trained binary model. + + >>> import gensim + +--------------- +Train the model +--------------- + +Here we train a word embedding using the Brown Corpus: + + >>> from nltk.corpus import brown + >>> train_set = brown.sents()[:10000] + >>> model = gensim.models.Word2Vec(train_set) + +It might take some time to train the model. So, after it is trained, it can be saved as follows: + + >>> model.save('brown.embedding') + >>> new_model = gensim.models.Word2Vec.load('brown.embedding') + +The model will be the list of words with their embedding. We can easily get the vector representation of a word. + + >>> len(new_model.wv['university']) + 100 + +There are some supporting functions already implemented in Gensim to manipulate with word embeddings. +For example, to compute the cosine similarity between 2 words: + + >>> new_model.wv.similarity('university','school') > 0.3 + True + +--------------------------- +Using the pre-trained model +--------------------------- + +NLTK includes a pre-trained model which is part of a model that is trained on 100 billion words from the Google News Dataset. +The full model is from https://code.google.com/p/word2vec/ (about 3 GB). + + >>> from nltk.data import find + >>> word2vec_sample = str(find('models/word2vec_sample/pruned.word2vec.txt')) + >>> model = gensim.models.KeyedVectors.load_word2vec_format(word2vec_sample, binary=False) + +We pruned the model to only include the most common words (~44k words). + + >>> len(model) + 43981 + +Each word is represented in the space of 300 dimensions: + + >>> len(model['university']) + 300 + +Finding the top n words that are similar to a target word is simple. The result is the list of n words with the score. + + >>> model.most_similar(positive=['university'], topn = 3) + [('universities', 0.70039...), ('faculty', 0.67809...), ('undergraduate', 0.65870...)] + +Finding a word that is not in a list is also supported, although, implementing this by yourself is simple. + + >>> model.doesnt_match('breakfast cereal dinner lunch'.split()) + 'cereal' + +Mikolov et al. (2013) figured out that word embedding captures much of syntactic and semantic regularities. For example, +the vector 'King - Man + Woman' is close to 'Queen' and 'Germany - Berlin + Paris' is close to 'France'. + + >>> model.most_similar(positive=['woman','king'], negative=['man'], topn = 1) + [('queen', 0.71181...)] + + >>> model.most_similar(positive=['Paris','Germany'], negative=['Berlin'], topn = 1) + [('France', 0.78840...)] + +We can visualize the word embeddings using t-SNE (https://lvdmaaten.github.io/tsne/). For this demonstration, we visualize the first 1000 words. + +| import numpy as np +| labels = [] +| count = 0 +| max_count = 1000 +| X = np.zeros(shape=(max_count,len(model['university']))) +| +| for term in model.index_to_key: +| X[count] = model[term] +| labels.append(term) +| count+= 1 +| if count >= max_count: break +| +| # It is recommended to use PCA first to reduce to ~50 dimensions +| from sklearn.decomposition import PCA +| pca = PCA(n_components=50) +| X_50 = pca.fit_transform(X) +| +| # Using TSNE to further reduce to 2 dimensions +| from sklearn.manifold import TSNE +| model_tsne = TSNE(n_components=2, random_state=0) +| Y = model_tsne.fit_transform(X_50) +| +| # Show the scatter plot +| import matplotlib.pyplot as plt +| plt.scatter(Y[:,0], Y[:,1], 20) +| +| # Add labels +| for label, x, y in zip(labels, Y[:, 0], Y[:, 1]): +| plt.annotate(label, xy = (x,y), xytext = (0, 0), textcoords = 'offset points', size = 10) +| +| plt.show() + +------------------------------ +Prune the trained binary model +------------------------------ + +Here is the supporting code to extract part of the binary model (GoogleNews-vectors-negative300.bin.gz) from https://code.google.com/p/word2vec/ +We use this code to get the `word2vec_sample` model. + +| import gensim +| # Load the binary model +| model = gensim.models.KeyedVectors.load_word2vec_format('GoogleNews-vectors-negative300.bin.gz', binary = True) +| +| # Only output word that appear in the Brown corpus +| from nltk.corpus import brown +| words = set(brown.words()) +| print(len(words)) +| +| # Output presented word to a temporary file +| out_file = 'pruned.word2vec.txt' +| with open(out_file,'w') as f: +| word_presented = words.intersection(model.index_to_key) +| f.write('{} {}\n'.format(len(word_presented),len(model['word']))) +| +| for word in word_presented: +| f.write('{} {}\n'.format(word, ' '.join(str(value) for value in model[word]))) diff --git a/nltk/test/gensim_fixt.py b/nltk/test/gensim_fixt.py new file mode 100644 index 0000000..c0f1870 --- /dev/null +++ b/nltk/test/gensim_fixt.py @@ -0,0 +1,4 @@ +def setup_module(): + import pytest + + pytest.importorskip("gensim") diff --git a/nltk/test/gluesemantics.doctest b/nltk/test/gluesemantics.doctest new file mode 100644 index 0000000..441de8d --- /dev/null +++ b/nltk/test/gluesemantics.doctest @@ -0,0 +1,383 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +============================================================================== + Glue Semantics +============================================================================== + + + +====================== +Linear logic +====================== + + >>> from nltk.sem import logic + >>> from nltk.sem.glue import * + >>> from nltk.sem.linearlogic import * + + >>> from nltk.sem.linearlogic import Expression + >>> read_expr = Expression.fromstring + +Parser + + >>> print(read_expr(r'f')) + f + >>> print(read_expr(r'(g -o f)')) + (g -o f) + >>> print(read_expr(r'(g -o (h -o f))')) + (g -o (h -o f)) + >>> print(read_expr(r'((g -o G) -o G)')) + ((g -o G) -o G) + >>> print(read_expr(r'(g -o f)(g)')) + (g -o f)(g) + >>> print(read_expr(r'((g -o G) -o G)((g -o f))')) + ((g -o G) -o G)((g -o f)) + +Simplify + + >>> print(read_expr(r'f').simplify()) + f + >>> print(read_expr(r'(g -o f)').simplify()) + (g -o f) + >>> print(read_expr(r'((g -o G) -o G)').simplify()) + ((g -o G) -o G) + >>> print(read_expr(r'(g -o f)(g)').simplify()) + f + >>> try: read_expr(r'(g -o f)(f)').simplify() + ... except LinearLogicApplicationException as e: print(e) + ... + Cannot apply (g -o f) to f. Cannot unify g with f given {} + >>> print(read_expr(r'(G -o f)(g)').simplify()) + f + >>> print(read_expr(r'((g -o G) -o G)((g -o f))').simplify()) + f + +Test BindingDict + + >>> h = ConstantExpression('h') + >>> g = ConstantExpression('g') + >>> f = ConstantExpression('f') + + >>> H = VariableExpression('H') + >>> G = VariableExpression('G') + >>> F = VariableExpression('F') + + >>> d1 = BindingDict({H: h}) + >>> d2 = BindingDict({F: f, G: F}) + >>> d12 = d1 + d2 + >>> all12 = ['%s: %s' % (v, d12[v]) for v in d12.d] + >>> all12.sort() + >>> print(all12) + ['F: f', 'G: f', 'H: h'] + + >>> BindingDict([(F,f),(G,g),(H,h)]) == BindingDict({F:f, G:g, H:h}) + True + + >>> d4 = BindingDict({F: f}) + >>> try: d4[F] = g + ... except VariableBindingException as e: print(e) + Variable F already bound to another value + +Test Unify + + >>> try: f.unify(g, BindingDict()) + ... except UnificationException as e: print(e) + ... + Cannot unify f with g given {} + + >>> f.unify(G, BindingDict()) == BindingDict({G: f}) + True + >>> try: f.unify(G, BindingDict({G: h})) + ... except UnificationException as e: print(e) + ... + Cannot unify f with G given {G: h} + >>> f.unify(G, BindingDict({G: f})) == BindingDict({G: f}) + True + >>> f.unify(G, BindingDict({H: f})) == BindingDict({G: f, H: f}) + True + + >>> G.unify(f, BindingDict()) == BindingDict({G: f}) + True + >>> try: G.unify(f, BindingDict({G: h})) + ... except UnificationException as e: print(e) + ... + Cannot unify G with f given {G: h} + >>> G.unify(f, BindingDict({G: f})) == BindingDict({G: f}) + True + >>> G.unify(f, BindingDict({H: f})) == BindingDict({G: f, H: f}) + True + + >>> G.unify(F, BindingDict()) == BindingDict({G: F}) + True + >>> try: G.unify(F, BindingDict({G: H})) + ... except UnificationException as e: print(e) + ... + Cannot unify G with F given {G: H} + >>> G.unify(F, BindingDict({G: F})) == BindingDict({G: F}) + True + >>> G.unify(F, BindingDict({H: F})) == BindingDict({G: F, H: F}) + True + +Test Compile + + >>> print(read_expr('g').compile_pos(Counter(), GlueFormula)) + (, []) + >>> print(read_expr('(g -o f)').compile_pos(Counter(), GlueFormula)) + (, []) + >>> print(read_expr('(g -o (h -o f))').compile_pos(Counter(), GlueFormula)) + (, []) + + +====================== +Glue +====================== + +Demo of "John walks" +-------------------- + + >>> john = GlueFormula("John", "g") + >>> print(john) + John : g + >>> walks = GlueFormula(r"\x.walks(x)", "(g -o f)") + >>> print(walks) + \x.walks(x) : (g -o f) + >>> print(walks.applyto(john)) + \x.walks(x)(John) : (g -o f)(g) + >>> print(walks.applyto(john).simplify()) + walks(John) : f + + +Demo of "A dog walks" +--------------------- + + >>> a = GlueFormula("\\P Q.some x.(P(x) and Q(x))", "((gv -o gr) -o ((g -o G) -o G))") + >>> print(a) + \P Q.exists x.(P(x) & Q(x)) : ((gv -o gr) -o ((g -o G) -o G)) + >>> man = GlueFormula(r"\x.man(x)", "(gv -o gr)") + >>> print(man) + \x.man(x) : (gv -o gr) + >>> walks = GlueFormula(r"\x.walks(x)", "(g -o f)") + >>> print(walks) + \x.walks(x) : (g -o f) + >>> a_man = a.applyto(man) + >>> print(a_man.simplify()) + \Q.exists x.(man(x) & Q(x)) : ((g -o G) -o G) + >>> a_man_walks = a_man.applyto(walks) + >>> print(a_man_walks.simplify()) + exists x.(man(x) & walks(x)) : f + + +Demo of 'every girl chases a dog' +--------------------------------- + +Individual words: + + >>> every = GlueFormula("\\P Q.all x.(P(x) -> Q(x))", "((gv -o gr) -o ((g -o G) -o G))") + >>> print(every) + \P Q.all x.(P(x) -> Q(x)) : ((gv -o gr) -o ((g -o G) -o G)) + >>> girl = GlueFormula(r"\x.girl(x)", "(gv -o gr)") + >>> print(girl) + \x.girl(x) : (gv -o gr) + >>> chases = GlueFormula(r"\x y.chases(x,y)", "(g -o (h -o f))") + >>> print(chases) + \x y.chases(x,y) : (g -o (h -o f)) + >>> a = GlueFormula("\\P Q.some x.(P(x) and Q(x))", "((hv -o hr) -o ((h -o H) -o H))") + >>> print(a) + \P Q.exists x.(P(x) & Q(x)) : ((hv -o hr) -o ((h -o H) -o H)) + >>> dog = GlueFormula(r"\x.dog(x)", "(hv -o hr)") + >>> print(dog) + \x.dog(x) : (hv -o hr) + +Noun Quantification can only be done one way: + + >>> every_girl = every.applyto(girl) + >>> print(every_girl.simplify()) + \Q.all x.(girl(x) -> Q(x)) : ((g -o G) -o G) + >>> a_dog = a.applyto(dog) + >>> print(a_dog.simplify()) + \Q.exists x.(dog(x) & Q(x)) : ((h -o H) -o H) + +The first reading is achieved by combining 'chases' with 'a dog' first. +Since 'a girl' requires something of the form '(h -o H)' we must +get rid of the 'g' in the glue of 'see'. We will do this with +the '-o elimination' rule. So, x1 will be our subject placeholder. + + >>> xPrime = GlueFormula("x1", "g") + >>> print(xPrime) + x1 : g + >>> xPrime_chases = chases.applyto(xPrime) + >>> print(xPrime_chases.simplify()) + \y.chases(x1,y) : (h -o f) + >>> xPrime_chases_a_dog = a_dog.applyto(xPrime_chases) + >>> print(xPrime_chases_a_dog.simplify()) + exists x.(dog(x) & chases(x1,x)) : f + +Now we can retract our subject placeholder using lambda-abstraction and +combine with the true subject. + + >>> chases_a_dog = xPrime_chases_a_dog.lambda_abstract(xPrime) + >>> print(chases_a_dog.simplify()) + \x1.exists x.(dog(x) & chases(x1,x)) : (g -o f) + >>> every_girl_chases_a_dog = every_girl.applyto(chases_a_dog) + >>> r1 = every_girl_chases_a_dog.simplify() + >>> r2 = GlueFormula(r'all x.(girl(x) -> exists z1.(dog(z1) & chases(x,z1)))', 'f') + >>> r1 == r2 + True + +The second reading is achieved by combining 'every girl' with 'chases' first. + + >>> xPrime = GlueFormula("x1", "g") + >>> print(xPrime) + x1 : g + >>> xPrime_chases = chases.applyto(xPrime) + >>> print(xPrime_chases.simplify()) + \y.chases(x1,y) : (h -o f) + >>> yPrime = GlueFormula("x2", "h") + >>> print(yPrime) + x2 : h + >>> xPrime_chases_yPrime = xPrime_chases.applyto(yPrime) + >>> print(xPrime_chases_yPrime.simplify()) + chases(x1,x2) : f + >>> chases_yPrime = xPrime_chases_yPrime.lambda_abstract(xPrime) + >>> print(chases_yPrime.simplify()) + \x1.chases(x1,x2) : (g -o f) + >>> every_girl_chases_yPrime = every_girl.applyto(chases_yPrime) + >>> print(every_girl_chases_yPrime.simplify()) + all x.(girl(x) -> chases(x,x2)) : f + >>> every_girl_chases = every_girl_chases_yPrime.lambda_abstract(yPrime) + >>> print(every_girl_chases.simplify()) + \x2.all x.(girl(x) -> chases(x,x2)) : (h -o f) + >>> every_girl_chases_a_dog = a_dog.applyto(every_girl_chases) + >>> r1 = every_girl_chases_a_dog.simplify() + >>> r2 = GlueFormula(r'exists x.(dog(x) & all z2.(girl(z2) -> chases(z2,x)))', 'f') + >>> r1 == r2 + True + + +Compilation +----------- + + >>> for cp in GlueFormula('m', '(b -o a)').compile(Counter()): print(cp) + m : (b -o a) : {1} + >>> for cp in GlueFormula('m', '((c -o b) -o a)').compile(Counter()): print(cp) + v1 : c : {1} + m : (b[1] -o a) : {2} + >>> for cp in GlueFormula('m', '((d -o (c -o b)) -o a)').compile(Counter()): print(cp) + v1 : c : {1} + v2 : d : {2} + m : (b[1, 2] -o a) : {3} + >>> for cp in GlueFormula('m', '((d -o e) -o ((c -o b) -o a))').compile(Counter()): print(cp) + v1 : d : {1} + v2 : c : {2} + m : (e[1] -o (b[2] -o a)) : {3} + >>> for cp in GlueFormula('m', '(((d -o c) -o b) -o a)').compile(Counter()): print(cp) + v1 : (d -o c) : {1} + m : (b[1] -o a) : {2} + >>> for cp in GlueFormula('m', '((((e -o d) -o c) -o b) -o a)').compile(Counter()): print(cp) + v1 : e : {1} + v2 : (d[1] -o c) : {2} + m : (b[2] -o a) : {3} + + +Demo of 'a man walks' using Compilation +--------------------------------------- + +Premises + + >>> a = GlueFormula('\\P Q.some x.(P(x) and Q(x))', '((gv -o gr) -o ((g -o G) -o G))') + >>> print(a) + \P Q.exists x.(P(x) & Q(x)) : ((gv -o gr) -o ((g -o G) -o G)) + + >>> man = GlueFormula('\\x.man(x)', '(gv -o gr)') + >>> print(man) + \x.man(x) : (gv -o gr) + + >>> walks = GlueFormula('\\x.walks(x)', '(g -o f)') + >>> print(walks) + \x.walks(x) : (g -o f) + +Compiled Premises: + + >>> counter = Counter() + >>> ahc = a.compile(counter) + >>> g1 = ahc[0] + >>> print(g1) + v1 : gv : {1} + >>> g2 = ahc[1] + >>> print(g2) + v2 : g : {2} + >>> g3 = ahc[2] + >>> print(g3) + \P Q.exists x.(P(x) & Q(x)) : (gr[1] -o (G[2] -o G)) : {3} + >>> g4 = man.compile(counter)[0] + >>> print(g4) + \x.man(x) : (gv -o gr) : {4} + >>> g5 = walks.compile(counter)[0] + >>> print(g5) + \x.walks(x) : (g -o f) : {5} + +Derivation: + + >>> g14 = g4.applyto(g1) + >>> print(g14.simplify()) + man(v1) : gr : {1, 4} + >>> g134 = g3.applyto(g14) + >>> print(g134.simplify()) + \Q.exists x.(man(x) & Q(x)) : (G[2] -o G) : {1, 3, 4} + >>> g25 = g5.applyto(g2) + >>> print(g25.simplify()) + walks(v2) : f : {2, 5} + >>> g12345 = g134.applyto(g25) + >>> print(g12345.simplify()) + exists x.(man(x) & walks(x)) : f : {1, 2, 3, 4, 5} + +--------------------------------- +Dependency Graph to Glue Formulas +--------------------------------- + >>> from nltk.corpus.reader.dependency import DependencyGraph + + >>> depgraph = DependencyGraph("""1 John _ NNP NNP _ 2 SUBJ _ _ + ... 2 sees _ VB VB _ 0 ROOT _ _ + ... 3 a _ ex_quant ex_quant _ 4 SPEC _ _ + ... 4 dog _ NN NN _ 2 OBJ _ _ + ... """) + >>> gfl = GlueDict('nltk:grammars/sample_grammars/glue.semtype').to_glueformula_list(depgraph) + >>> print(gfl) # doctest: +SKIP + [\x y.sees(x,y) : (f -o (i -o g)), + \x.dog(x) : (iv -o ir), + \P Q.exists x.(P(x) & Q(x)) : ((iv -o ir) -o ((i -o I3) -o I3)), + \P Q.exists x.(P(x) & Q(x)) : ((fv -o fr) -o ((f -o F4) -o F4)), + \x.John(x) : (fv -o fr)] + >>> glue = Glue() + >>> for r in sorted([r.simplify().normalize() for r in glue.get_readings(glue.gfl_to_compiled(gfl))], key=str): + ... print(r) + exists z1.(John(z1) & exists z2.(dog(z2) & sees(z1,z2))) + exists z1.(dog(z1) & exists z2.(John(z2) & sees(z2,z1))) + +----------------------------------- +Dependency Graph to LFG f-structure +----------------------------------- + >>> from nltk.sem.lfg import FStructure + + >>> fstruct = FStructure.read_depgraph(depgraph) + + >>> print(fstruct) # doctest: +SKIP + f:[pred 'sees' + obj h:[pred 'dog' + spec 'a'] + subj g:[pred 'John']] + + >>> fstruct.to_depgraph().tree().pprint() + (sees (dog a) John) + +--------------------------------- +LFG f-structure to Glue +--------------------------------- + >>> fstruct.to_glueformula_list(GlueDict('nltk:grammars/sample_grammars/glue.semtype')) # doctest: +SKIP + [\x y.sees(x,y) : (i -o (g -o f)), + \x.dog(x) : (gv -o gr), + \P Q.exists x.(P(x) & Q(x)) : ((gv -o gr) -o ((g -o G3) -o G3)), + \P Q.exists x.(P(x) & Q(x)) : ((iv -o ir) -o ((i -o I4) -o I4)), + \x.John(x) : (iv -o ir)] + +.. see gluesemantics_malt.doctest for more diff --git a/nltk/test/gluesemantics_malt.doctest b/nltk/test/gluesemantics_malt.doctest new file mode 100644 index 0000000..d87050e --- /dev/null +++ b/nltk/test/gluesemantics_malt.doctest @@ -0,0 +1,69 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +.. see also: gluesemantics.doctest + +============================================================================== + Glue Semantics +============================================================================== + + >>> from nltk.test.gluesemantics_malt_fixt import setup_module + >>> setup_module() + + >>> from nltk.sem.glue import * + >>> nltk.sem.logic._counter._value = 0 + +-------------------------------- +Initialize the Dependency Parser +-------------------------------- + >>> from nltk.parse.malt import MaltParser + + >>> tagger = RegexpTagger( + ... [('^(John|Mary)$', 'NNP'), + ... ('^(sees|chases)$', 'VB'), + ... ('^(a)$', 'ex_quant'), + ... ('^(every)$', 'univ_quant'), + ... ('^(girl|dog)$', 'NN') + ... ]).tag + >>> depparser = MaltParser(tagger=tagger) + +-------------------- +Automated Derivation +-------------------- + >>> glue = Glue(depparser=depparser) + >>> readings = glue.parse_to_meaning('every girl chases a dog'.split()) + >>> for reading in sorted([r.simplify().normalize() for r in readings], key=str): + ... print(reading.normalize()) + all z1.(girl(z1) -> exists z2.(dog(z2) & chases(z1,z2))) + exists z1.(dog(z1) & all z2.(girl(z2) -> chases(z2,z1))) + + >>> drtglue = DrtGlue(depparser=depparser) + >>> readings = drtglue.parse_to_meaning('every girl chases a dog'.split()) + >>> for reading in sorted([r.simplify().normalize() for r in readings], key=str): + ... print(reading) + ([],[(([z1],[girl(z1)]) -> ([z2],[dog(z2), chases(z1,z2)]))]) + ([z1],[dog(z1), (([z2],[girl(z2)]) -> ([],[chases(z2,z1)]))]) + +-------------- +With inference +-------------- + +Checking for equality of two DRSs is very useful when generating readings of a sentence. +For example, the ``glue`` module generates two readings for the sentence +*John sees Mary*: + + >>> from nltk.sem.glue import DrtGlue + >>> readings = drtglue.parse_to_meaning('John sees Mary'.split()) + >>> for drs in sorted([r.simplify().normalize() for r in readings], key=str): + ... print(drs) + ([z1,z2],[John(z1), Mary(z2), sees(z1,z2)]) + ([z1,z2],[Mary(z1), John(z2), sees(z2,z1)]) + +However, it is easy to tell that these two readings are logically the +same, and therefore one of them is superfluous. We can use the theorem prover +to determine this equivalence, and then delete one of them. A particular +theorem prover may be specified, or the argument may be left off to use the +default. + + >>> readings[0].equiv(readings[1]) + True diff --git a/nltk/test/gluesemantics_malt_fixt.py b/nltk/test/gluesemantics_malt_fixt.py new file mode 100644 index 0000000..b802641 --- /dev/null +++ b/nltk/test/gluesemantics_malt_fixt.py @@ -0,0 +1,9 @@ +def setup_module(): + import pytest + + from nltk.parse.malt import MaltParser + + try: + depparser = MaltParser() + except (AssertionError, LookupError) as e: + pytest.skip("MaltParser is not available") diff --git a/nltk/test/grammar.doctest b/nltk/test/grammar.doctest new file mode 100644 index 0000000..7c9cf7a --- /dev/null +++ b/nltk/test/grammar.doctest @@ -0,0 +1,88 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +=============== +Grammar Parsing +=============== + +Grammars can be parsed from strings: + + >>> from nltk import CFG + >>> grammar = CFG.fromstring(""" + ... S -> NP VP + ... PP -> P NP + ... NP -> Det N | NP PP + ... VP -> V NP | VP PP + ... Det -> 'a' | 'the' + ... N -> 'dog' | 'cat' + ... V -> 'chased' | 'sat' + ... P -> 'on' | 'in' + ... """) + >>> grammar + + >>> grammar.start() + S + >>> grammar.productions() + [S -> NP VP, PP -> P NP, NP -> Det N, NP -> NP PP, VP -> V NP, VP -> VP PP, + Det -> 'a', Det -> 'the', N -> 'dog', N -> 'cat', V -> 'chased', V -> 'sat', + P -> 'on', P -> 'in'] + +Probabilistic CFGs: + + >>> from nltk import PCFG + >>> toy_pcfg1 = PCFG.fromstring(""" + ... S -> NP VP [1.0] + ... NP -> Det N [0.5] | NP PP [0.25] | 'John' [0.1] | 'I' [0.15] + ... Det -> 'the' [0.8] | 'my' [0.2] + ... N -> 'man' [0.5] | 'telescope' [0.5] + ... VP -> VP PP [0.1] | V NP [0.7] | V [0.2] + ... V -> 'ate' [0.35] | 'saw' [0.65] + ... PP -> P NP [1.0] + ... P -> 'with' [0.61] | 'under' [0.39] + ... """) + +Chomsky Normal Form grammar (Test for bug 474) + + >>> g = CFG.fromstring("VP^ -> VBP NP^") + >>> g.productions()[0].lhs() + VP^ + +Grammars can contain both empty strings and empty productions: + + >>> from nltk.grammar import CFG + >>> from nltk.parse.generate import generate + >>> grammar = CFG.fromstring(""" + ... S -> A B + ... A -> 'a' + ... # An empty string: + ... B -> 'b' | '' + ... """) + >>> list(generate(grammar)) + [['a', 'b'], ['a', '']] + >>> grammar = CFG.fromstring(""" + ... S -> A B + ... A -> 'a' + ... # An empty production: + ... B -> 'b' | + ... """) + >>> list(generate(grammar)) + [['a', 'b'], ['a']] + +Grammars with mixed rules can be converted into Chomsky Normal Form: + + >>> from nltk import CFG + >>> grammar = CFG.fromstring(""" + ... S -> NP VP + ... PP -> P NP + ... NP -> NP PP P + ... NP -> 'the' Nom | 'a' Nom + ... VP -> V NP | VP PP + ... Det -> 'a' | 'the' + ... Nom -> 'dog' | 'cat' + ... V -> 'chased' | 'sat' + ... P -> 'on' | 'in' + ... """) + >>> grammar + + >>> grammar.chomsky_normal_form() + diff --git a/nltk/test/grammartestsuites.doctest b/nltk/test/grammartestsuites.doctest new file mode 100644 index 0000000..0cf81a5 --- /dev/null +++ b/nltk/test/grammartestsuites.doctest @@ -0,0 +1,109 @@ +.. Copyright (C) 2001-2026 NLTK Project +.. For license information, see LICENSE.TXT + +========================== + Test Suites for Grammars +========================== + +Sentences in the test suite are divided into two classes: + +- grammatical (*accept*) and +- ungrammatical (*reject*). + +If a sentence should parse according to the grammar, the value of +``trees`` will be a non-empty list. If a sentence should be rejected +according to the grammar, then the value of ``trees`` will be ``None``. + + >>> from nltk.parse import TestGrammar + >>> germantest1 = {} + >>> germantest1['doc'] = "Tests for person agreement" + >>> germantest1['accept'] = [ + ... 'ich komme', + ... 'ich sehe mich', + ... 'du kommst', + ... 'du siehst mich', + ... 'sie kommt', + ... 'sie sieht mich', + ... 'ihr kommt', + ... 'wir kommen', + ... 'sie kommen', + ... 'du magst mich', + ... 'er mag mich', + ... 'du folgst mir', + ... 'sie hilft mir', + ... ] + >>> germantest1['reject'] = [ + ... 'ich kommt', + ... 'ich kommst', + ... 'ich siehst mich', + ... 'du komme', + ... 'du sehe mich', + ... 'du kommt', + ... 'er komme', + ... 'er siehst mich', + ... 'wir komme', + ... 'wir kommst', + ... 'die Katzen kommst', + ... 'sie komme', + ... 'sie kommst', + ... 'du mag mich', + ... 'er magst mich', + ... 'du folgt mir', + ... 'sie hilfst mir', + ... ] + >>> germantest2 = {} + >>> germantest2['doc'] = "Tests for number agreement" + >>> germantest2['accept'] = [ + ... 'der Hund kommt', + ... 'die Hunde kommen', + ... 'ich komme', + ... 'wir kommen', + ... 'ich sehe die Katzen', + ... 'ich folge den Katzen', + ... 'ich sehe die Katzen', + ... 'ich folge den Katzen', + ... 'wir sehen die Katzen', + ... 'wir folgen den Katzen' + ... ] + >>> germantest2['reject'] = [ + ... 'ich kommen', + ... 'wir komme', + ... 'der Hunde kommt', + ... 'der Hunde kommen', + ... 'die Katzen kommt', + ... 'ich sehe der Hunde', + ... 'ich folge den Hund', + ... 'ich sehen der Hunde', + ... 'ich folgen den Hund', + ... 'wir sehe die Katzen', + ... 'wir folge den Katzen' + ... ] + >>> germantest3 = {} + >>> germantest3['doc'] = "Tests for case government and subcategorization" + >>> germantest3['accept'] = [ + ... 'der Hund sieht mich', + ... 'der Hund kommt', + ... 'ich sehe den Hund', + ... 'ich helfe dem Hund', + ... ] + >>> germantest3['reject'] = [ + ... 'ich sehe', + ... 'ich helfe', + ... 'ich komme den Hund', + ... 'ich sehe den Hund die Katzen', + ... 'du hilfst mich', + ... 'du siehst mir', + ... 'du siehst ich', + ... 'der Hunde kommt mich', + ... 'die Hunde sehe die Hunde', + ... 'der Hund sehe die Hunde', + ... 'ich hilft den Hund', + ... 'ich hilft der Hund', + ... 'ich sehe dem Hund', + ... ] + >>> germantestsuites = [germantest1, germantest2, germantest3] + >>> tester = TestGrammar('grammars/book_grammars/german.fcfg', germantestsuites) + >>> tester.run() + Tests for person agreement: All tests passed! + Tests for number agreement: All tests passed! + Tests for case government and subcategorization: All tests passed! diff --git a/nltk/test/images/twitter_app1.tiff b/nltk/test/images/twitter_app1.tiff new file mode 100644 index 0000000000000000000000000000000000000000..6c0de2dcbc5151f08a84735c21b2718b10c5b784 GIT binary patch literal 51896 zcmXV$1zVJDw1wXphVE_{y1RSm?k=UfK^h#oyAhCXq*G$(ZX^_u5|Azt6!CoLI_EDu z*Lv34`(9g96JQ5`0VBA~uJ$#A1RFJ4&RVC(PM8`uUcp9Jw9<%+G+D`3x98AbiZ)%v zPEU+6SCb`M&0eq9uFH}qU&BFPta9C5q)5wAzxVJVOr})F$v~WmBt^MW&)J~QUbtAh z*1*M}hl-%axY5YfuM# zu|v0I^QN1>xm5Lrd-t|Sfce1DgMPy|uRsfF7Sfc-L!TgvK}V6|#S`5gYML73rj2v| z5X-^ifPrtNMc4one4qM=SG>9jQo zD*#n`DS#RUYyxP&ne0z;0G00y4+1RF0;MepYhJ|w=pH2!4v3;(!n9ae+Jj^0R=XxnN3Mtbq`u#Q@ z59P%|+o3{&lc)aL1aQEOO)6Hq01+}(D4f(W^8_UPH3B>}nGOz};(M?11-$|dcpRh# z2os}N0Xj(VR}z;AR)9>B1&&6~8}`8X9Qwu0@UtH+1@QmRF2w59fNO$xg zg{A8N*4Q^}MKYj2(iAal4*3lSgpg04wqN3uNQ)Lh_te~zj-6jF146$3Lya3hxD5*3 zd8=IW>FgmSZ0BFxgr#hm#!D&CHt@{`ST~fNEu;qTvhzC>$L#_XWGsc^#K7s)15wZM z@^cv$h`lfQJ@Y7pI}HKqh~|EJKYhJ!-hacS#+Hf2=Rvn%?(DDWL!ZfQ*lUt!t6l3ZzdEsRcO z#h{o?ZDrxhf678VjfUi+Mq)HV72{tiMjAfz#I<#HxK{3P6np`QPg)HLzq<1Yqtms@cjVfmXFe6(PrMeLV zK!*$&6u8&T+y$%!&DO^|Jt=s9ePx>aeJk8L|44O!|6Qzo-9A7* zIs6IWBmX&Ks;)Z z0w92C9eTXH?*m7v>+UZJIRQWs&=!C=79>yXQ0iE>9}Gtgs@I1jWk4O^NU9`q0H}p` z?q_&9X!WO*8GRV_m+5l1t9N&d*uG`yN|oPK=U4#{~{r zi2?w8F90Bk6q=G$_Duj;srtz45nNMFty3(b1wrFZk7a$I5}F3aew+6WjKDhP(uX$;LLCq?jI#ap-m_dBskD7TFp>0^zPl(^VUn<$d(=n<>%cSK5_XQE16ql(Y)x8m}`9wfz$lhrJ zHoQ$D;{X6o;^>xjXw+tXgRN{;8r1gO7t5uDCK?V<_{GPx@Lvxn?VozufV`T#aw;HK>^W9VnO>BOnvMqX(dUGT-x%m%jk{MZ@y$euPd@6|(dIccIU!^ME zOMt<57b`49v6#|ZDgaP|G#I#G4F6}5PMwhzyCh`z2DFhH zaZtb3WiGL2PXypAY6TF*-H1dCTc!n%Y)a6%y) zSv2>3DV~=cV{R{kmGi^!xgewzKu^(GA+ZUrNvtQ58ZP)qytn-&ZEs)P!wW%#02C5+ z%K`9IunMdaTnghw-#)>kO>)%N`Kw<)%G?C4*maYG_S5ExzJQ#`MB@rex(p>9&O0jAr7jZ&W?(A0)S2U!EnGW@yj~{`YA#TK}q$PLT1JDc%aR} zPB`{j1AvsK6YnDB(Y@G=pmAUTU>-#P{hAN}+)=Uv)su*2(VJ|7R4G{{+~$h_;nA!Q##?|x2? zv=X+lLi6wCgF(NTj8Qj`sc@!eqU3rU64Jqi*HECiNMKsb-DjLb>ts$C04KCrifj?a z82-(=O`={Zg#iO(V3qf{^hoAW0F6PcO8JV7GC`h8_^Y1v_XkPOTieh<+Fmj_q!<IU7#O+!Wrei!=U*iLAer4` zC1&SyBY-gj5K?+aE(Fdz_nio$*FEB4ygl@!I28k_`nMs%x#(1iN0D;%ZWiK5o1Px*|un|TXVBtts=Ex>60GZ<`+a(*{ z!5W5=?gFT=L(AP=ROEYTipn=AK!xyG-;>6H#;=ZG*}7-%MltRq0Kx@+=m!_|ACUG4 z{`Pqkzq$N%C_q%!X93?NMBYK2_3(){^JA*?i>ZgYt6*ztp=eKitKg^ILy%$vIP)!l zmBad;63%sHHt5!oaYq2k`bq(hO13_-lo8pn3}*z#kxm1ffq)newg5>IAO`sq6ewdB zFk$zW_Ma`k??bO|aCIKP&17>8gE2=ADXKKEc<4>F*$81E!YG3O6szS?YqNN*Udzlr zE85#2{U&)6$4(LeeMXO#CIb3TQk}^;mbzCWCC4Q0H2ExOi1W04&ciW@hKCvrmGKIlUtLLw@h`8iBT}h zP-HYwqH(6x#L@zyS*3y_iF2aReiP4$Zz<2aXfWI%y=LLn^Gfr-T^uofTj1JU1ZSBC6i##;LvH9 zL;bKT&vAg@j(t?z8!*NVDx>yH=dvrwo-jpQ2zDt5dmr!9Z5(T}IveUcHt$UJTT;5# z_w@Kx^j08>n|4}Vtp|J~JA6wBAt9eWDPI;P-zqIXAp^fYGt;#KGrW-urI6VAGbckS zX{nD{X*7}o8iO8~kIsS`;WbyLr|c;QwcaJS0_D(mC2P*As~a3Ovo&3QIzlVLDlL3#U(S|4I83E$yl3bF0VMoN0{r>P zh;qDxNKB~GNF*!G@R}!GKeZhh?JiozuGZmR2-cn`jX*Y$e!gFuny8W_uC=t~&7H)Z zy(IrUwfptjULOSBRYHfw-{L6l6>M+~e|GWsR#CWy_d~248(mYI=MEf+-_@3;MpSuD zJIt>*jG~DE+M}Y#5kLnF(5&3SqT}7(wID^juv;6`9G;Qw6H2vzlV;qLPN|WeiI5Ta zCFUn1UvKe#4Hw-{nFUM`JUm9pE}2RX0m1np^=QBvV=S0tjp!POObFVP|W?VhaczKK_N-8L1)(hn6@zr~;9Xq4k> z^vTnh*w@%Fz&QFR|A+&*?7A%AM`lMhRUQmRHlc#$^I1^<M!|w zkl{ivEIZEN0sr+nQ5e@X7CQ~i!sQh`rEax&G|Uf2s1XP7waQVIU#qdYZ_v8`K)>M9 z=1*z83m`{y6XS26kXT?c`QSUR|CiVQO@D;7!XLZHI{j4u5*C{#Hd`=*t-m<_TA8Ea zEgdlrh5uVa*AD=2{m}}bvSCI~o5Gy`>tvv9y-p|PcjvgmdXl1oo1#T=Ggn`F!dnUs z0;2(OZUE?4((lfToz~0N1LqgE*)IZU0Exi{ zwGSziPwuR)_iICvRnPfcm%DXyKx02mlZM*bG$x>|tbOO3K#UWO6ZXP>}8FKmVWBuEt`r0v+(6`5K_mskwRgkKNwYoiba6E~=SoM>gN4Q|UHSSSPv(9jK= zS?%JM2|~M$KKY+X`nM*rO8LJrfEoj9=PwMTED@V;eYQwr0tDmp_ACvEGZJYtMtCv_ z=2LPPGeq}%ZZ&;Hd;^2@b4-nM3e0o1UgwhAXa^)Rm$elAw-4-1anf;8j#N@c{0au! zkXwZsL!H1atn@e!+=Re-Brgb-&x8M=ub0oBs09ESTkdJmG9eUhR7SVtqCP!F!{E4z zJJ>KQi-O^dK=N>b&LpB3UmJQ#kX$kRnWW+YYmZk97RQP=`wj`|o&J-|vVa{Ca^{A; z-IhQs=*(V?hUV6l`KPhl)7Wz9{kOC?Z#&f;3pTV3^|N!M(zkZC>9X-xlkroT=>%=D zY(5rW) z!+-bDiw|CyGFA9LrTlV57vsnww{QOZuZ0q+#T>Qe9cD|f6LZ84Eqye~u#cnI+kAQ} zEI2QltT1XIBYkiqZea_>l}ml;&T1*~2RRHj1O}~LD3LXGI;qle&WT9w5+s;Q*o>D> zb=3tFk+AkY#XA#c&)>_afhdZE&bTRrv{RnTRKD;++_cWpdX&AwnuCI){A!MFPPSQ9hEbQb zRUZ#%0W;A7A5l~%L}nYa5^eU*V^||nVKDOJRoLeW^x&_|YaebGd53nH+)P>G`}q_o zXG&JlP;Et0%iDl$b`is7)S;TXXQ)aEXZ2&6eti@uL5wup)7AV$ry zN8JUe0k(IUSfe9oy~u48-`_C!iSa;&m8g-lRMx!dq_G8(Nvx1YzDzgf-CO>8virE~ zyN_BKg;aUmi1x(}>zxxcHTehlnjD5?VFCSYE5GJf*DPMA(7>edgad!f{y|p>^>J=` zOc++UlPvbt5kMFn-8^ROQTs$%o}4W$~UZwOcN~dl;@R{7Q>X+VFdX^7ql0Bi@=J5!>l)p^BfTo87Ha zFrr!V+E+@B0X02L9#9=)?n4`C@?(TSJ)Kk#6PS&cpy9Qz*5X8McERIQICR36yS{yC zw+ACA$h)Py&}7JHAw`7Q7m&yAQVT!z6wytx;uS>QwAs@)PRZy(g+ddVH1iT%@mMKig}Fe8#K{U7+T{I zD&0Ld9Eu)ofd=Ik4$C=~ccK=m#%WA2yat`F0aSCWP^6ChaDWwaWa@d)n%9FZ6^1l3{sKs| zWbA?hxnE5vlflqYOXG4;aXBz;Sm?4uLl? zrLj`uKv3X288~r@>Fl|v4#2X!+!8IvM3kh&pgWx$tIU)&WAC#WMrY$)&SueK?QC)i z@MkE%P_sb*h=LI2G_T!iCzsi)Be8|3$|&*F_Ld8Y)H}qDWSYPdAf;^!{?(F}g_FUU zz^{|Tl&VMbuPz=>ilcn-0E%jL%bRSM3ej)?ShMfngRwJ!kX=V-$boN;jxGf zEcih-6GJteKL-#U1oCDeU;sbY1nSc(JpjcLk`J~5DrM5)KnwAPYNA77>coFRk43k4 z2oTvv6jOtSi>%~R_q~9xRmbQ?g{e$E&bYi&LAP!!a;FA(uqJ(zygC#%VL3P?!QVq! zo3ayx2LOeHU*m(iOlm5&)NCh}uURYsWfzQ4z@esrv6luXRF zD{dx-Y@dVQR~?s?Xu**{1fc+=Ku74)kBAjZd?>J~wZaHekOx3!rD?llBUo`N1n62EvmmReGm56QWMH zaUXXUJ1)W{<&BZYXv}z`uTEcY`dikxZ!`Vf2>KRCL-NbRv-caQq~hrjRC*H3PCWXx zXOJJTbq*H?Y@p9SU)~Pg{e$D7fMEX>8&-m-5qQkVupV+GD?i*wJSB8lQ8r}#$Utqj zWY1bTX$jO}F$$z!xg_fuX#~(heO6k4BsKLUI09FW)ym?Y7`wU**K?5 znrO0cmR%eOyq7B0>HHy|o|`S16`6*SD3DbLgq~kY2G+I|z+FC-RknJf`|etjv5ItSy#x0OB=*r(6lBbzyTH#y=W`yih5|f-LniryblcL7MHcF zqcCIqKPJlT_AcxCmRi0Nz!e=s0z#ERw`@$fCglKXrPY6k4xOWvOlE`C7t^j5mFgOx z9bx#Rd~5LrPxk>t^oV2w*U~{0yy0QGLCMFzwXUu`S3Q`<&FA~Aq$yS>0wS>G&QzDVa0U|=<(>Qpv_}IS*r5bi}xc)kG5o`r^)-&YdF4zE#3Fe5W!gUI)>mT+(n*k);0hH(Gh!g z4>;8?WGglWBs^&F@HHt=3ebbvH|T4zNMg(8jlsDpk1LuOyng&eMtz%^sJpw05l#|B z!@rR+VUAd{7Q_*@>!K5;L--@vR;omtTeI&Ln!1?BO+J#_y1hwjBT&%1u zoz5|RMro+!M`@gK}@TM}lwIo}NR~P9hnIpD!zgV1mU*PCw9KMaDYJ#^g%Q#sLu}#0pzw218 z39?CYTS?~>?=F!^vSVt~D8asC0Ic2^l`Z%F}57Xby`CvV8otXULV=>?P}R z+)C7Ewj8ywdUUmJI<5W2s!j`z4P@)rRbf&I(q(5?AsNvNPaBk z*#D%4h6cyPb^i4T#sc0BS+#fp5C|w1z7L?@lm8=)h1=l&SBK&gC&}$zpA>14qb14E zwgjGF0bsDwA%6T^DmDd3osLkIs4avgYodDD1tR=LB3Fe~PYtpXqw;p96u~yn?}{EX z!In`cxI^-itbKG>FyN0*ShVo^>IWoMiZ~MP-uVtPXiFP8V(+KguViUaybbejXrhR4 zFP~w;Gpo$SdWo^UA(qexzOye}p~KxS_T<8>Sro{oqAGs*6D|)cRhjb$O4yX6&18Fo zpxb4c)aA|!Q_u;dFLq?0kLgtrr2|)gzV0Yd+jp8B++&3Ci?yCx(G`apqNEANI@x!* zS{}*ej^wUnBf-}Iq23$F&wPtXWx+Nq$8<|_D=SF8q1((2`f*N?i z3=l?+WRgPJ4}_x}T6meMAf^>AS#4GiPT`Glq9WeyNurU=IhjxQL&R-4k*4vWY3y z3ZVePsbdZ33@9#eWD>|473cjVA0WAMD)>Yo+<}xN+w8}MbHsITURu1oP_C}BU*J2P z&@Y@B6+ z3tP)uxyK3_Epv-u)n9$Mp=XT)PaNNFSJ_i`-R9CY2)WVO0kHJu?_M$ z`Qa%1OjB^LmXFjiV95Nw?fzm2`<{a|ns@V*>RHeroaUM*_rM&rtbfBDq2;8lbkKQb zp?_WfLrqBR6WVowa5nGH01o4F#?vaTO{zV_^U0fX7Mw%Oy|gZ$u7G-8K?@@BW~CAvN%ZinBc?skj5m_FI>W$iI5^W zUTZZw5--IFzLAq~d7DUy#D;mf+>=Ia%4rwr;K$jmXS!Y!0+4;{CF-;^h&}*`S}K_|LBkTF2%q;%C~8HJli& z2!u$8+9?eVy%ekE+|@-Dma#MfjrBhGOpg7<)5uCHB79+-GW`Xzq)Jj+5v-niN?qBt z%hs95)u4*RP{^VxX7dx*86w2^_7x8~d|dWfE#E)y{rQJPW^p9OaZ3Dxo#FTgK3sR492>7Q9Xq{Q?i)a^2BvCM^cSS5p&*C z)X~p1#K+=c#jKy{r@@n|C%TFy`cDswCgm%KbCA;P>|S=$3|T_=z{=>~5u^DNDktJ~ z$*0n>jGLR8=T(HMKFnzdTWh6yzYq_@%6YxYGiaj%Yq{{m=bBNBRaJZLMwda=+#n83 zY^uTc3BY*XL&&KW431JLJ>^ zK8%|7;#?*YqN$T@y*2mPwtEIYx2L|7}JEAi@y)^%=6$ykoy!!d~4t!*s37+7i7m<%U;n6dVl0-4?qFZ?-V zPz0f_&KZXMeJ;xE`I(8syg;dmkk#lkfe4=_$aFUTS%~fXrC7VQt!|R|7E|eQE|@*p zcn=kZ?koN2h(wY1hGylizSb$T&{IE5o#RliIc@mC7U1Jv?6X6n`ipK{p4-jn6yPbY zX;hj?_lEY1UA)Ht3tS~Oy4NeuLgqxElj0~`B4i_ihzkv9d-+3;-jbEs#=cOk^re6e z#VRv&cDN|I+|!QK+QII`h(?4!+We=2*Ykl!-iJ2}zbM_bMm}Zd-;kA@(%&x9P$>y6 z#4&`d6Kq?eS0rQ$ef{#H`HhQf#Ba8-;CFy_ouuk;e08NQgjPQfTfax>Ra4mKp{g_b zw@m_DN;@hWAv`JmwnFCt?;L+wzbQ_dO4gd93*i_zABXsL#kzhDaV@G05*Q*9L2uaR zJoQvg;r+l_u>G+p@A5q%LRIM^^U((gigG=8x6=!tuV{K>VegCla+vRr`gE;%T*0gK z_r@$B)|mTk5&A#&{X$zG@5q>m4z6=ns<(1HmgUC1EVOI=W%;*N+Oq-y;@_WF^Gj15 z(SwXS7fVQG?&gs%Ua*CK4&Y@zKtU@k!Wqxvv^Cl^r8e`$E~kZ2B&R#=W52gh&iYrr z=NFqiV8-iUyPR-2T+O>_%_e@n+>W0QytxSH*NohB$lP4GJ;C`s=uw!7^Ro5BZ+--} zhq=A8*mfN`cVlC+(@YjPKju=q%YgmA72}0A|JOy)e696mN8qMe;XdW|{v-vGw!V@I zeprY@)ix=(hUYhZL|dRl^Yq%#V*Y!{-RKEl<)8$e{X-@2+|tosMFx zd0xjXJ}jmf+h`^XrE=!)3Rn@MoGP2Cg!O4*vnq&ZCwTrmn)gE5%{6GUoY;8mA;aD3JaDL$Kl-oGztnBf;3&NsZS0*sz2a0@;_hEaJ=&j2*WlYvPPAjD_o4Ni;djo!#1_qB z_|2x|H;Ba^7FjQ@*S~p2$jQOV+osPvm(QQNzZAbk4_;)$t)ObAWW6$aY@3)xVO?@1 zpvyE`CD4+br&vY+3qRahxtZy0MbDktefYLx`9l?MSJYb1pjr#ZH@E(L54!lz_dehU zJPv4uHT38gosKqL;#if-C$1GzrV$DH$RzqH1@25iZb-IE? zppFN*m`8Pwr>TnPu&`Fnk~eWk{MML{>0D=(OkIFlO80=D--(Y4ZDo~9?x>4(HABD$ z#O32H!;ck}iO*xVTTQ4hL_l$=$0XDaW4yCwAc!CW>u@n)3*XAUEvFVqjT4!K(B4H# z5XP~%QZo}|4udVlGCf*+eDa$!q}X56<9%?>PLse+V{JO-&gy^L^h=UEd%Ueg?6#6) z4OjiLh65Et`|cYJ|0wgyw1gk7^vWF1y~#+241XN5%mOJl;bY+1jYRV>^Cyg(%P=%Q z5|Iv0%Y8mMLKLy_kLvdvvTJ;D_ktGX^ZbNAb?74$glK7rRGP9i1r#zAg>a>lhm`!# zStpH^3|v`#wT(JF*nKTniTu@E6O>oPgjZxm1QQsunkj0VRrQ%!iQcITq!9R3pcB== z)*3YU>BRYiI9e&ytMAnB66Xavx&6>J+7ShUE3EVm^uF_&ZRk?`O;VZdTA6ROM(-rj zepvpbnGSzwJV^PIAw@d3)zWQ7j)0;rU9Yz9^-Ofmh=BSl`UKX0qUn?lgSpQ~R#u1~RmRT={PB7Sm-#ao6Iwd}KHcY(# znF}MNkN4sZ0Vn3~lN?+v2&FcPRQ-3ofmQ2+($#>{WY=fSUO+udWx_76-{?!6MQTlg z=H_wRPomSICRrhf>`ID_-(YTQSLN~x-`j9m2*7sFqx0gJ%Z-w^_gB&CrqWY4abQ@p z7S`&_eU+k26j6broi*`R<&x8=LxFEY&+w1qIk^*etFj6tN;t&}L%`W6`b~M9^Lpu; zQ59QT4~&$tmZVMh2-7vorilS4%^hJRnHX_qmttmf$7^?4-_$%xaNn2pVEyAwY%|20 z%4+wr*YrnsN}YE0n@GO-rDV2o^Q!BfF7I9F_b@j(QlC5B zaK(1t*f2Za``~Gbx5iWz!p5$7MY3pn4x~o)4d>qeu5Z~S{1s{ab7yJRL*0#i3c3TF z0)mA<5WHQ_2sC*e7_}F;kryQLT8OkDR8+*X`%zZ@M83OLVSM4DyN&l@*4yW!JH0IY zBF%k7)`R45p58f(&nUY3k0<8G5M=dm(ZJLWN?+q~cmQ>oaQ3fxkFB*Xx2v5d~YM_GIz>VyvFO>f=~$B8+nq z^1242T0>S$6Gbl)1I%+o<~qBVJsR%ioz)n7hJ>((Gan}$3H~NetaScDe{0O*FdmlD z{a}-iR1m11XMzEJtn#pN&I3Sc0RlOyG#&UKc5d{ig zD43s9xWxU+v*D`D9bd?}eColc!-TuNK+jsZIqa5}`kd6gM(#BPkYV+Gbz2u;O4C3} z)MAA;h78}1;jVlzjsFqms=<&FxR}1x6u&iG*8`aUn5lO(j#;LWZ zM(i^5P*bas<9)&6`%M)EL(ftLeG4ag(R~F+6>&9r^|E}+vi0L~VW8ofSk?PCPcSb} zpJ2mp(Vsv5w%Wg?%NDB4j9x9EBbM2xT z6xHAv^)Hz=W-G4>H{dx6jub>{E88NSiX8bF9pX1-NCIVMzvILy@+fsh@b^8+N68Ag zTCrut{aRbowb5pJEAHM?e`Kj#`oQ*wr=#4t-C%d~k9hh!nQG>y4mGKcY-hng?-Hg0 z-kSxp_q~-ie;xD17XPDe?GFL*tq--3Z%uZ|6 z3L+nYNt6w+{&J$MPh%MpqHIsYDo)`n{t#0;weQ>?6wu%IG%`I}MA5@10h#@XK8PDP zXdypjeqL=RF__ugCM20k$vSHRaK7Ueut1lO%kDOp@+mTqh_Q1Er8dSe9TEN3DNj4{ z(R`HBv5NqIG!u1n!9|+VL;0iBnA1gfURn3HZVxf_Z~%T!fuom&_2ie06}DvdDvsXC z(Mj=t1ID4op}`fQZ&`zmOUf5h-PlW0X}T3|HPc(J0_W*1oqpXfr5Qp}jetRjR=xZBzvyYggk?WaPOe=@bMcd`VM+ z|B}7(6T;@3V>VjDKoUgC-YD^r+bDfeo)*82Tfh}VTTIzVmpR{P>TV!_6T3POgJ0~V zl5y{pCYb%rf|Eon?;qknsXLey)N2umOdI-dth2uw`Ay2au)snb8i*40lW$%#+a3%A zNlQK1zf#J#9R1GE(9J%^tlG3Po1~7GdS^@iY1Cb)gzZ3;a=q9s*|tdGt6#&8_Cawt z$KOc9fw)>$0I~XWu$^v81A8m0eQW&qv)K4&kHGd3nrnp z>DTjv;S4X1TQ4qyG@cYCZ`Ntbj)%k^kZ9Md!dEQ4PqNC)c2MuGa~9c+UJbs>m7_f+ z_uZmCivOAyhjVuZ`Eq80eik!yHuCxly3iNR*7M?l^Lylz`>TsA5|O{loYE3MrNb;= z&UOT9zR}#PygYpq@_+(pL}(}g2!Qd58lXS_dplePpt(tc|GfohuQvN(AhhU_UC(PY z3_98vsaIZ5MiOyIvhv>2H9(-s6RQEwJTair6p34;*Eu;i6aNo*GvFSdb1Mo(jN9CA zTFMnLWG*=v{AOgg)sLS#xn}L;u2N|?J;4{J@{l(wH9Zup3(Z`WD;{%OVY^`70+*T` z5V775wj;$(vbHH-pdm+sRA~|9enzw|^`=(-kNQu)^e5qLUXm96LrU%68~hS;UBa-I z_7G4pCbc??2}LK7vz=r7jOYWO&e>>Iq__{e->$>fqHxE)ziR&?a={7tlQh(F+aJBr z8eRd1ccxPPcLSOUy!M>KBFugP?fmqnG+bv>mNoQ>XMt(0xVtf1_ zCH9ryz4d1X_LLkS7}QBxa6e9T$ni7=V2QZv9X=wiR^Kpk>dJ9Z^4#h};wv>c3I0{O z>63jVK#>ctQJUYsEGxTOBdJlHmn=LIU4@z?i)oZcCY9b0jFUD_?$Z>tYWxxXNhct07X>(Rl)ioMc0m*+ zy4rW%OFQ=nLFyL_c`W{rNk5jZ*M!XdBz88IUI29R{lXB$#{6rbVt6N9Zi0?YNYkjp zMc+cNzX=W>P<})yW{!`Xp z)Iq1ih%;H%!1B9*Ml(~#s+ib*bnK?VOjh}h;LoY>HQ~!{uMfFlhDO$<#8%Ii^_EV3 zMt59(bNf9PgjRA%nh6D+#$U7Sk1qbccT$&-cKO1MIl`alW-Rp4vx1bs@Rh}8rfkk0 znhj6D37S!V`%e7di_q6FY+N_wZXYyft4SL)pH3z=gM&C|5>N3Sq z$7SsnN{k5i3x$0ab8o13j%6sfrSLuxfqg;yed9&!v2-YL+dRes=seUlMx;Wq`S7gj zPXY#NO}cQoT;aPeOO%+^&T8R}*FKDTSU0V8N^`nIBg?$V#aiWtFgwb`Dv!h}j_x-W zJNk01p3$miJ;*)ayKW|M8S=ks} zQ1o=FWT5} zii}HhJ{fyF`DwbgmCR*&F}L|UwB*l1I^V#Vv?@jrtwOwM8{5N70R?QVbVd{67S>}U zt-B&2EXZ)9o0T8=BV}t|-445%z+S_dfPloTQkQk~ZqugFTvF}$x^diq(*yPKSv+<7 zEw`Fq(Uc%5ErJ+dFG|Jp(+^9)9&LQGr&l*C!6qbjkTRpIyaI}405#-IXJJS$XCFQ@ z;rZY{B}$T}9pfXJLohHI-0<|uAc=EdzBaoR5QD?MbUqsD$olYRUlVI-au(r8B0EP4M&OE44m`PL;4C+EJ^zrcmzy;pBrg zLvE)}WkZexud;aPi{%l>Vg{ckq*Om2s}(Vv0z^Hzna4sJm8S8GYdgMDlOOGxm`wZa z3s*VEt{mOJD!(&MFUv^W-qDfLQN#)a8LBl26aOt%H1uiacNtOomLM+1lG(1%x1`lz zP-^_Mvonhy)3f8fs^MF6jqq`5HFRO- zg;teW^t6)V5t$9*we0|J8T^zv$sd*qdamXAmxVw2Ce+R;1j#iH{H*$Vc^|BQY~_0M zIrblG-G6UHE^Z&}NznFP{Pe<0@3zUFfy(Ol zUO~$x(T9A~7stdKcJC6Z5r5&^85@ZZ-<^grlwkP z)W`iymO|b^Q!cS& zV&=&!I6xi$R?h6l{1?{QESg`hUB7lq)i@Li_0OYaX|NE2NG5NG+A?t%m@@c1NqIHh z`x3UE)w8^*-no!h&#id_?S%w(QPv`2^~@NUa443n$~w5LL}n-}2Fu=*i-O-_ECF}; zHUYC;msb8H$7*u%-I;U%AMR^oh6(J>B>ov?$^*%}KXRRYU8CPOpA&8V$kz-iReYPo zt@!n zCr8EAPo#2L{K~mbvUo?mrW{_vo1V?^B4dttZ@%3xJIAAc*sm5VX-+5Unv*0jg+{+V zXf7$)^1grCs{deLoIr~(eR6;RkwCeUC$J`^Yn!mTJ`jSE$g#yM{~7ns7nm*KkY4S; z<|%I0H_~^|zS%25!n;0((PZBlNdqileM;#p*%5OmgdX!?`8JE}2dUMI){hSqu2)Tj z*GW+~?`MZ)pGajixcZSCu+%;eg;a`#G~@)%RMOWHH#`&i{+00hTXS$Lv0{s12bTC9 zBa(FvQy&?MD&Qz$q_#C1uH8d41g3tOp_)pR?8lLAWW}YzBT2@`a^a)pS4&|el2X}` zvLKQ2CKwJSBIHqj=&<}4BTy$z>W1z#STP5ZMl!PSQnNmvPuOMS_g+-XX%2zs55 zP|pc>ol#wdIKz2}I-8e3WD(67UqWM-kX!fEcF!bcl)Oc+;@D@|G4*OjS0%f}cFR|>TdcTKD13uj`r(br^7_uAxlEP{6uR)Ds;y|(#PN(#-Q z5A{s;(W3*1BXmfi`O3rNl3bovF~TwgHT*!+-c zFr~Ui;@r{%HntjwPUqxd={aYjZ&H^@jfZnv6*Eg$*HzcnXc%J6;neP$Sjp}T9Cp}Ub%y1To(Q5w{t8;0&~l#~)s=~fy60Ra&KQ9@e4 z&G)zVvG-cXT036u7yDm$j{Cl!>pIWtIzJ!zr#(;tjTT9!}9eDtSNqkHgdo;Fzu#ENu{ldfch=*DVy^5-M-Xd68{IB;c z7q!8p(yRtzvLbuoPMIg&X*LOSws3O--{AF-A`yaRcHLO_tlcnaQIE`Vv3}rF$jssp zTs7Kq&f#YfDH(nd`{Lmdn(TP4J9ko%Cyq!dhWRcIo-5QmS~9?@SHR!mX<~dZ@iscX zLnB9wJanY*9&0ms3N{-5lo%h}iFF0e+v@HaF4a&K6x;uudfZQqu1(E?F4m^Tp6Qva zeecv;X-_N_xl33y#7I}H5$gSg^ViJ;-|da=0BMBMn4|I&X)X1I_o`qw6L-p?-nz$0 zEE4ua2A-9Q^TQN{4sH3hx~Kj$GHdx|DMewWGEo&g7W<|m)m`J+oKf~Bvp?~DF|qB# z1jvgk3%51vmMrzV-cOdUiWsaV+NQ|d>wK(+t#>z?3Yeh>v#(msddSE%8-xoT5$-W0 zRFUS`f6S>dzOd(-OScVSwl4;ifAM&}PHr)V%OvlKp?_#-YG#@D%5Ck;Isx;8Xr=5L zcFr38yBX<0L;MFr-&QLLwyeKr5%zg~C%^CcgsD%AMGwu8q9~s9Cqb^G#Ww|=x0l?} zbGcm?>+-+WTT@G#k4zUX*W*5}+^yp4OI?4K2(c~_tex4n{>5zJP@rZuy)b0?4gW2xoV!Ks#-tQyxp3x4Y}otAnkQOdLw<{zcVZ#59I0N%DTH6r{_pT+_^77n#|%JgclA zRXZZJ3cu>M-n}v#-evm!!?2if{KjCLyv5oIspa|0eobcLG%K#6M5L%|yKpo?ajVhO z#@g|n?1;=)RD(Ul)+Bv#PU_mOV)YxouDzw(3XAz5sgXtG;3v%SZ|@5J-SO$(%HYI&EsoOHd^^8E zKLk~$9(FZxvgZ%2!)cj#eSyaUmig+_HqKCOw_Ew98T?^jOSwOo%^?QPEDUZn0L9@Lrn zUj}dXC6-)O^Y`uM*1O_8gHPDUnY=^%3pT!3-M_{8w6=fH%DJ>LLo<57R5FS4(gNB% z;cV)|%XF z({mX~_9^~!DmuN7h4(uZ&r>AJRUsg=iP$|RCr{>LKg$l5l3$*^xS4?g_m4BdnCtb$ z3B9mkX>RY)V=8ZLerwT-XAIqEOFe%cU;oZ9IC`sJREyz!BzCm<9Yv%96?sJutHYWb zP0K6A>w3i$UC|u;Pdz`>7TcvymYJIb=HWkQuxzWX?()4hN8$_Fag;x zLR`A6(k!y>1tMk5B(t#iIyt;8^)neTA+`gy&!>~RGc>w3i86VHF)Xjm(}9{cPctw@ zIbieKc?!B}zB)K<;XuJv4bFx4;n0A6yehkcu z%1~jA!|Kx8Ljva?Qf2R@c~gM#&yt19D*W4dUNe5`oPZl*OHar?CzO(T1gqS+c)|BUUGvTAY2I8Mmd z-k)CXv`9lDnS<4rtt}>h?h&TX!y3HUI$iUt3P5sy^JIxTdjg;M{r$Y1TtWHweDtMa zMI6T6E%1;r({v1j0ifp}03voe8ayz7k|F%@_x@q2;Ym4KTQ&&3LVsz{g^$=l;YV=1>Q~3N-apK zxH0_eN(^28SiX<@`n)m@LG-inm6}BaD>fRO&pJNfSIjm6t}LtJpFmfMCKL+R)@#VU ze$BQCUb^F|zoE!r;X+D+8sK;l9q-r=wl6woF=V#*d<>LMWIX@kM2ov#-wY$Xk?&Z< z6C|0>6^JGdPRb92qM1K%W0cNL#bqKcWj}InuXSohexT)e6XkN=kG5c#@8Tc2V{2=q zMQ6U%mn;G#Qi=#(4!-)?Tm@*YX75X4b8>+{?UB8Rd`Fg`C^st-=5#e%O1n>3e%y+4 z*N4Km{V{H6G0`TaU>d6l#OdCgDP<1)qT%?XuQeyoYG?_a?XVNFM(SCXel4FTNfoMt z{Y@xkIxs_pGSubMy$na`Ei_4ugL{jck`3@KO8R&X!kXd}DE=^RYM6klK848~AuqO# zhE%Y(tKUjwXYEcf*M}5dnw&>+uZ!-}M086SNGeL3U9)ueuUsQavU*lnNIvyVHCif^ z#rc4LPk54u;97Il-8`S3NRWJLhBt$WmTbbSazSJ(RHRq<@H8ZT`zcb|_BLQ`C37NP zy+?xY+EZ12C?H@`nE52k=LlYk#C=oQS@fqvto;?aT6xfVdY!5Jxlo(qk$)A4hoYe% zO_yRqwb7kAQro-jlFZh7?^1H?rKPbQW8en+J3Ww9i~h^7&dc?`FNdipV1=7wy@}7t zg3Kj8Y|RlM`)jzJI3o|nSOqV>1^w`=bUR-Cq5eJ7%P`|a+si#s^ZC-djDV*XP8oz@-_zH9$?ykk` zF&?(bT$z&i)Mw(+bKSFQzn+Z?1WQmVy<1Ay)ctuJ!MFXe8jJhd<~99ty|T;Q+_hN{ zo=QfGqCVDYM!(n*{x;8?(p|Cqw0R@bK3j~=4n-T>O=I!k9e8SLV+t1PqFdO7b=)ux z(sLn1*FJU-J}G{5@9JB1|9D=;5I5t*E$w6a@R)>d{u&3WI_sY@dB9q6p;!2U$X?OY zkkn0kZ!dn>A@bs3#Cbm`V#fBpY$rKlXV6vpINnW8Ws<5ijNLZj-MHNRsd9-pB1YNv zg1LvXPsKRR&sa;Ci4<^e zWYDDVs0>tGZZ%I44VDQFAu(k;`RxpaG?KwjIK{m+KhI6uMtHwuR>I=b3wcawR>E3w zh1mq|Hclx2s)D?RuveIK$DyI>b!0J*p1LVg*0(dpYVz@a=aqHUli`&A#n>6`X839< zpqOmS*??qiP>o45fm859{uhE9cvPeLB5bP|=Z6gqeO!Pd4}BWdOys_PH9HiWr?88C zAi*+J3u|nh;|+%!t^PEx+pGhVI}4e|tGy=+GtHGxg|%zSzj7+>ZV)%)0-nkF&FWdY zErA^k>a@~Hge>Dit0yeh`Q*(y63HtdFEL*KVcc?`Riw4XSWhBXN4tS zgAw9#yW#y!uQnGLyAQ$$G77^rOZ?^8@837LdE*_WMnO6Vwa2xT6o@g|rTz5QSJ@g* z*6!r+xWyzp+%7~s!PiT5F+rZ(;x)g);X>nd6fI4`D_N(OkCmfYJj$yTlcqfSi=%mr zrUH`g888Mdl@)m|!S8>3*f1Kq@=w1xPNuzjMjQC}ZM+G`_vz@p5!dds&(!PP{*Axy z-`CP6oEm8S37&cX;hje4lWiVDO%iBSeVx^7!&m3!O}~>q9EqtgtU;fvSV0-Dkm{uC z0;D!fy^T$Ty2cIi(Hy_4T1WKy-7-H}`4UtI<$vHkSj(_KZ@d_;e?y73uy1-+_F1%FS?L_X7H`i8r!vp^R#aC`#nGaT@gn`%-*G1P(W@`1b`A$HIMfa+aHJP>9HAob%UIk} znE62?W+Bu4LCMxBLgtS#*fO3opQRT>Fg(HLCOA^0{g&aG!p#U-Y6IBGelL;~MUMb^4^X_lk z7xUm3-syVNtsl2vaPpPP^G#Urp#ui!I0kkE*%VYTWMX4Bn0E&5lk0aXH>M8B5Yusz z@lY+r09&qnxvCDSQH3W_ZI{l4rxiz-@pYJrUffdy=5|Bt(6|#Yvv>^6ymI3uv(L4B z{_h=JK;9krQGfL9{+QoSE=n%60w=!N=P%^=x*0Jr#DB_mChQ)T6tl@S%`aCT_vC2# zaeOUOTZWBK0kNNZ;V2L0Gq@nMB;a$HjbygiS>pgJ9n~g66*hRi!#uqs($G?yXwXBP zk}#w&LkAazp$R(LyWKffcVK}v)j{M9K?Eqy+ANaBOH#g0lJAj(O-pY+UEK}5xQ84| zHvJ$Oz=XO#2S!gTr}6%^Aox>90sfa{#8gMVUP)=vAO~O2*a6ox4U-NlyAG#tY@Yh#*}uAR=qLvdv6j+ePdw*L z-b7B~Kr`xtMaH-K>8PI+{gMi6=HK)z?W?}KNT|~Gb8=jhdu@9%VvCnY)&B&X{-p&dHHOcheuEJyxf_<272KJusAzi9G^2c z|7HH;N`#Ol@=oMDPG-)?q7)#zo(+Z|%1B5;ZcagzdI{1k2e&hKg8F%+f11sFC+X+H zGM?t&3m9_}{v&u(*RXS}Sn`D6hbdjlb5&( zLKI1I`ioOh=R@l`|1;p}pf>H?&7T>2ezeQ#v5G4xIm0~Fw+woA-7P`>alvQXI-4+} zaBuRVJM58daYaw;!NbvVlB7(B!(SthFe<(5lGxpHKWT_glZqZiCEQfah(!q3%L_LV zX|N~C2_gm0*)*(~KLOKp%(homa-VUw2bSbls||mCh#41#6V?P@VontTuU$aaNYjh@( z`nkIQ5L?^SfI+m9;X5U;wh`G^o2D#1+_|9%v|(hjX-#ROQ>(@~3BvE6G=|Bvqgk>; z%H*uRE-weucA9S6XItU7D)#Ocm<)-k0YdG>`q74_c3TDzHzq?R<17t%Wko|os^UkK z$f2rE2afcFyB0DlRE!kO93*A zbvE-Fwyk8;6lr#VzWB@?s>iBoa~@`Kmd?I7&a_a-Oji06N9A)mpW0-gmB^)4@vq1Q-;CT>v7N)=BGIABTB{E7X-CLL$6&zZ`)sp+@ zILd5(F63ah?;-ldIfu!0(BCmY*hWM&r(IHu!qGs~?LtJ|gheWR(LR71Y1lEWMQ+bw z=#O(2S5|WOl9!NhV{8pEe2p9u zNwVqSj&>5`x~CY}Pbn9bF$|w}E^Siu*kO3v$<03dEVFwk9gw5yAu44NmFR#7_YY$8 z%*FS=(A9f=6cDKF_uJn0{7GQIwL!KIpQs<#BPWERsSCNa=2C31-Y=siOeJM)W8*6K ztH6Mq#_x7tO<*I8}c@zaetUu;2Blx;n|ubIvmOlfCLIMB`3e zj>k3jJr_Ktqn~?a=2+IclDorHz!uIYzMr>BfiNwQdSGO}5<=v8FVZnm7lj3xD{(4}{nmyfDMQ%TS{CO@YJ=}EoGUOF_j z^jUJV+~Je!lGkK{sx)u~8fNZ%b_>vT`4MB|6hgo1At=C{kNDj4W4sB?TgHY{v;yQ8awmhNS*t^`- z{9uWiJTJ;7L+4jCNdkG5e&FNNgeR|VisadLxbw%&beKI=(K!P=ZzTkBL#T5*&$3N3N>*QeIQH*8kQ)%M>B)Y` z8f|$kIb0C!Xdn9V^@DCHYTFRH($KR$!fT&dJ{3TF;DN(dZ1(59+2x0B>khA5_C6ef zaB16{$bJ%~y@n{y<~6Ck9dqC5vDYfL?9v%Hzh-DmKSjF;iFX6c6>n!eM{r|A>kZQR z^u#mK9%76lkveKP2O4Y*;Ms0-Pg8W4Q0xK76jX z?!sZ5gD|Hk7ku8^O;7a|K2Vs73_`62e_r*PaM6bpzFA68oh^!xwk-MJu5vK&1ut4` z{yK$2s&SGB&WXnEZIsAU-e+wbm*?a?j7VPma+ETvmY-)+AtIpuv$r_J?uD{#O8dLv zJKP!iKtgG(jrDJ0ZRE4ug5{4At&dOd#ScWi@kDlg=LWvc6Fr|-LYTWvX z+($+f`ht273%L>Pf{pF(d9czS&8BYg+ksrz-zgc>!#^VBq*C68n27*FNPF0q%DXHs3m~`b=@l z54??%kGIY*@pq&9zetytjy{*QdV5*Ivd`##`7dxUUUtQ*XhTWyfQ|Y`QSho&`|3OA zpVI$EN}hiyejlNHE|U6YO{KHWa_VO_>AHBT&ij?+^^K>Gw@y~{yE=kTFus1}O#SNc z@5trX*K_yHC!ogr)Xf0^2nQIae_R1j43NyGj}FEKAq*IVYaioj@sURM_wijVg9RwV zhqg;$XG&DJFJ(~ZE(Z!-aLSa!9J>ku?DmBUN{2mjaBZWUVqC7}(L4`A{Sjz4Lja1JAj7L$V;?8KU(^ieH!z zBFk?;36*X3V}!}^XR_hQHMjBM$@4FW5y&^6Jt0yMAX6q$Xc5*XQxq^TrciA0v!+rK z$aJPrYH9PPQ~tlmn8BAxcldvsF_XWc`R5^HM(a)6k(9R3!sTP_H)sE3bcD&VcpX!tqG>lw6*Xd4)c7#bTHw)Q&#n;A9xE1MgOECyN`x1YbXF%cz?wKM4uNp(R_vks?b$q5gkL!2cs9U?1p1bK_(56KRlR_M<)Mv1*&?n6L-XuX%6>iKayG2hl%u zvE^s+6p4q@xHU8%|B$;aGnM^-8=Tx7^8y1+#;QlD|kyOJh{G-0dy4WYu zr0?RN&}aP;)1*`9($g7-cTYO>-+^RjGI{XjXUR0l8gvXS>6Pcml37&eGV5`b<`zDR zXwGN;lGYw080S7|(GTfG# zdB3sDY*4j0KLV7Y5iS6V14Z!>`jGnw5O0m_*`iDZq!uLt#sK z?g;0$>64oFTJC)P@MfLR2B6^l8y+1!Xak_(kTm;Kaj8aYp@L68j`H6*C@%HNqOjSb zlXjPh047S@k7#}K51aopw!$yO^6`JL6+33XGXMJbeR#UL?sZx4)$YlQKAYyWsRzh~ zXp#uYFh3|4pOk)5a1^dtDiT3`t~) zDUUvsCLALE9}eVjnujGNiePTlt>I^4)CnK%g4GM!KH!#)wW{?IIl&mQ0vBX#wp9xs z4s>WxyUcWJbw&nR{i`IgC}5jlpQ_=UIEf;AN%y@AK*m?>0f99?HX*Di%<#-7C=g^= z^8Y-LGi3Of|8*cAk7#X`YBb`$iF%1!*$g=2AMOlgklO%-d}%^v?4g+QRDfm1oQvp; zs-8m;yP94ph)W%I+io~nJM_VLN>~zojXPZ@<<4HgBw+(!$XG#*jWn|ySF$~eoL4E5 zt6W#J8yehnIfIXb04(v%&`Nv&FEk$x>~N+Mnu+4z49NhxN&P{<3r4JMsRxHL4!{Ki z3RgjNHlQdw>Qc~LNU040nrGrv+@!FpqB}FKyjDsCaz=D zihwTobX%EK5+rx24^rbX-2kASf9C zFC>RI_X6_74@uT0{fqH-qcxV{i(xvp&Sttdf&XpSN+?M8}tuZO(66*Ixt z064!602rc{TLy4SZ-@j)LI>Idh%ktLrG?)B5$pg7Se7}Ke=P{MDNLBwtAPn9XgfeyqK>P{ z$6RvBqT0N1ZStUavP!H~I%&z@P$`7XQuqMifhSuEVD{zCBTwKtf;d(xdQ5z~I9jtjZbPl9VvmbIITUexlADdvrbok(#eX z-oL?*0t{ef6MkuZFybg1fEr9a2(xi1#Pp?Oe2{4I1WYoFXC)fyZ<8_M7mP5fETUxA z9MZH?tbtm9tUO8*1z0kr#$_9~cYj*>EAKMwU&TgIys+A1?ml+4MTz#|h>aE019XI0 z%h_e12QyI0bK-7etpiK*2LIo~D?O_B@%i1HS9@=1n_@H5_-rE>T$i6wx;7_8&XEC- zU;?E4tv9WX8+md(y~~(o6;rFQ#35jSElT1~?5afw0M%3jG5|PBXK2w;*%;dv3g3Z8 z)pr$%mf9%kNC_BFZNEjR@LR$nj2NOFLpT&$d9k*PcI1MXIw0yW5xK~qiMv8>$Cel-WnLtGQZXcPi-VPox+}&S0c~0kKFluR_g~2ieyW<5+#nHYiSPSE9Rt8WYF;QleSy=T`zWC!oRgSi zxn#w`>P769wJP|z2qGu{48UFN8SBy}-kt)mWSO(aAaZ!*_Bs-Id2CWV= z13RNEhYA(IxWpT%+roq;Yaqhv^Yp>+hB3(3=Hd1`0GKjEi&4*X1hb7RU?mdlII^l7 zirj{y%ZqTC9lp42DM)4Ufun6REh z2Mr{w(S<33^3m24!tF5YAj_K}>d0wtXQi398oo|YW&y>Ku;>Ag+-p8?prSoji6%JVKjpN0xp;P+BM+B^t; zGPaJj5wb(0HUN&c8nV%0H^t*hiGSI18Hs0- z(hm@_pTPh#Ya6O=rYqU-E~>E&LSRQv0|(iFGqY7aE5G@Rj||zoji>X2p!qj=<}_yJ zny?Q4W}OhKjv=ataR}8Lg5O%zJ})x+_iUK#h-(3&ekVEG-w;SXRk(?P z=E$|LKc#OlWy^EQlW-~o`*r!J>jQo%j|(=YPvjp7u*>i$5yCDbXji2?7)U|gmIZ(N zajQFL>+UfEbU{0|jK#8wls&-oqd-nqX_xBeB@7E^0_Gu!d``-J>W2Dxxp4P>$Z9@8Ocu8p+5&I*2ne5c^ z8EK4%JIss3htJ6d%q4SwdkyY$$3gl|(+eyC#H&WS6iD140AxV+$_!JG2C=oN2H%54 z<(ou^-u>YHd{~DB6S4^gvbDZI%tZhsvFzpI9E97??65c@@EUnt@HSoXBOW+RE>Yoq znD%?vM1xd%D?F|1Jf~Z{f;*utaon>>(*o$;hyc2+5CBNPer1ju!A9ZhPI2-c-gsN% z7J~Gq0SMvn1G!2!wGi1}GLIk%us#CqhaC`f7+HrD~r~ zNWu_}X%x8pDYY!gQoqq>1d{m|>s$jbe69;#T1dPzMc5ZADsMu!3Bm7Rc)8@Y?pjUD0LmV^sp!AQIifBH-^h8g9! zq^fA8!Q#?NBV%^bFlSEe&1!6-bSO*)vZqjlyMlX8?*Rcct&)saa z^eTX1ayAAy3^6hNL3jWS!g%(2kJMiSg&ZU@*s)}8Fa`kmIbjpHV#0hWzO)Gz7rp@k z2TRI`OMz-Ge}Do{)kDf9KwoWQz~v!Zw~iMLz&d)nKw@${lmJ-81{=?#1|JSywasA$ zDD_|UfqJdQ(#<>rzJ#TJ_v^9ZB`R2KUHAF0?sY2BZr5(n|4upD2DGe206g~bK?{?o zJV|7#kE#L5_nJ2_N&M3$qdLFW)jml!AH<2xNfQN>nN+2pcg91x00KPb+yh2)u47v-Tbe!1)z7rk831lRATNxPOM7aU2FIfqGF0TQ*t z1VEo06X2Li{Uy80fCRLB4EVzD zya{;-{L;AxAb9I)57A1`4jqZNa{&>u8eAfcW>8UOr0HIC1@3Bgdy6OE()LepcujvB zK-j<822jkZSpj%EeiK4i{p9*DH>n6P!#8tz@gujWwEnA`rH|dFN@0oL&TZs@j|FL@ zB$V$>VzDCk@g6@f7=EBLBn-)I2~1V%+?H42%$aheCzjsQ7LJv=@u{kK;3(uImZE$f zm{C3{jnt6BI1BdYBfWEkSsFKn_`maGUH2ks)my0uS|bkjygq2=UDN`CUwF1|Kv*c? zOe)p}bQZ#>j>5wbQUXz^J2&i(#2&Ic-Jcw@n=tbpMbB)|^3Wtz`MPD_H;nSqJy6Zc zuuf*|0Ss8Kn6|Ht(Ia6NSQnMuowniPd6Bo}=L`B&$CZnsCeJUI^wa+98)FQ@@!tv# zRI40xK_d_EZPn9!r2gT(2^mucC+lPlm~O6Z=apebc=E1c(j^;v0K`?%G9YqDO+Vox`)Pa)8&&#Q-!eR}jS<$@r(#FAjx|2|okCX$Sm$ zz{I0y7zm;RaK`Y*0zft~dKn>yFvUsVVbDmc6P2ypHw17@8aOhbkAa()#I`>8DCk}S z)=$MJu1fm`uk^U}S{+wn2Qrjb9^L?D=^8VYZA6PCbS1n4; zfNk3LkLd97x@ujZu;czkeufF48F}TYVX%&rKMNtTtrbcP8lyT|wo6KTXs?>L3)pRf zF$~>=6}sQ1rag_&F~k2Knulm&W#8b)5vWA^H{`Gaa>}tyCxS-hgb>V3Ra#$u3^=b~ zW0gaUGl;UWR8)?hh3g=?v0lsRu!8`y>NK>Onmn~bJmIkGKU#94f2wpP@Tv*Vf?O=I z)uFwD=sxFom=OnK+5^w%f%Hb&mU5u$o3C;k27s)nzqmwGzkWo6HM+nGj<}D?km`a8 zDBMM=aOSWjJx@+ACy-lC5zAWsVw|@i$AurzM7|IDy!y|^gQ~o_w0^M6_UbYI)gGO; zi5p&;pXyNJLG|~(Ffe3?f7sT&rcG_h?2D!QujrNzqfe{m=ANFv`mxHBXcDCmN+%>C z2!`FHgy_Fv(Ue{Bl#m$&T`M^lXU$I@Way9OzoKi9#H#1dT1zzZ`lCkeqr6T#fEf3^ zB}V%;FWJ*nJ@96@eBUby6u=ola}llF|DCp?A3K6yz|rfw-%GSKeM;K^v7DM^&7r^# zc1%banaDa5ijJXHzydjc6D?$a?9E_7-9{>STP?(JgiKr70get$ffO*o^r>0vrzYX8lyX5s?F(W(U5gI zVmfKGz20mdN>xlx-Nl~J`W_p0%_vCL)=|7rBS`ls`l%(395sKLy_&AV%pfVW_3!wh zF36qXMV~@aP~^-nghFs;rFW2bGI1*a=y1uwtr3$AP^C8BShIX~z~v)-rEq#wRFMBp zo^oDXvXB>texh{GB>yN*)^iz@ZcJkd}w3s94(4 z;0vuwwCDPVu0ZN{Yds}?yW50ypz3NSS(v~Me2G#=g%Ol*5SwnNqn4LP)L)M&yY;$r z_-d5;n}v6Z?}qf~H5QXluz!T(4S$Bq95cP$iAfat_?6C^3NjHtUM3Q_`0&9);kQ=G zq`TPw0Q1HGhUbQ6`f?jK5lw_|F%%?V6ifxM0w@^C>_8|6RdsV|0VzO`GanM%Yjvmp z%0HzIz|gi}qHD@NJ%d7%avutOOGtzbKNtmEzEO&MsJsAPMfU@muBk|rOfqu}JSJ^T z(A#Sx?UC-}XfPl5NyN+;(n`jFGxFq!tCIWi7&`=noC0u(wtI($=lE?+R8XK?`ht|h z+A*`j`DOrXa@1I!B&NAk?mfUU`{5Eoxy}?Y`uXciTV_;2SF}uJglrI#PN_tAs_a6~ zaBZ3q0_AiiMrRb`7iW5`c>Pa#|IWJU;>FkXzyBhEog~5xf!m=34m41mxq=gG zYefy|HIP%vkI*Z8ohz1LR8_`M^_}5l6ta(CsECiK*iVp`ub24NDrrXvmH~mI#K7O0 zUu}~JlztL8SW?<-B;IlYfCH@B%|>sh9yPQwn0eHWTQf4?Em!u2dZB{shWHR28Y95d zX_bs82mmZTAB8BE{nqRQv>-d{9!SuT2b|CsR0#|jQ9Ts-5T5&Oa}=}+dC1E_o~ZbZ zICsJ8(PNo<6l~ihL7XK+^*c$PL8B>8j`}Vsb>=5mkpj&hqkO$!9*@TT109=|FH1d5w zId2+(&D#6!jD!<#u0>oU=K0q8<&eqAJx4Qlky#Vd-7Pfgq z6u?QMC4&Xo#)L%5G3h3=7Uy|V94D5LTzB$7$xDRUxH zJh*yBu!d4lKv$j8iOxd8kiA-Vgxo&&Z~U!Ri~pl~u~_QliLk}#j+cKu-NxZttA*PK z^l8wHzo7$2VlY_g!sFI4<#ZP0+!b{*{oyIR(c^F_0AoePi93>40*J~@B&wKdgOcI_ z3N-_fOeKgBxxT9Zfif&24$zl(m7LNC2z4~_%wXTiV$`k0?4$4S~!NI3OIU z#UUq$wgItPevNnFXG-!^*%e&0c~3C8*0im^c>eCC{mR9=9vVQ{@@weV#bOeRkRsx6 zALFc2kb_aS?h8~NC|o7EK6wvbP2*_t8BZb3=iZXbc&hS=lb|u%#-C_NYxF z;q2Z6P<4a0!UG^6UV+HKE@LBaA^C8ASU4^wM$Ha%D8iX!Y)4ZEV#Kb$g`p5Cz;8j-oQxW2><<}3OBc$Gl(PXeMuj4{bPWpwl`gJ(J z6WO4uUbUS@aGIdnP8(8ixp`_pK&aYaK0m|AT{E~v)M?>MK}LW#$x5==53+BC`Z9?h zIh;-?#o3BT(*w1xx1^ji{}g8|g=z;M&#B#BmtIWu(u> z`v=+5$I-27VYL|GOx8!78@*aYgNzP-sWxPFbQ;}kS-59Cdm%#8Z5(o9<-Qo&ly&jJ z_%F!D20x(Lh+@zrl-SP0FtH_DbkxKg+x{>sqcz)T(lk`S$z!n~<3j0^>0enFPx6Yk z?Cg27P#t?$!@BnDj?ZR)Z9P1*-gIOtO`DthdU-DPbY@?CHUEnmYIxV*l|%8(A~fx( zm*M2wD6v-Ffu{f=01gmJ zXV18J8W&OIXx1#a`I~h;`(ptNJn=7(uKw)Vx#ksU-n|J4x=(Z(mqHsrmftvU9f& zu>SGw_PtcoEheZB&6N<=PbB=m8|Hm|QYLyUp7H|}iPyr?hFfkQ4D-|^SHw8Ue+={Fg;3TBm_a1_MEdL3 zlPagZ|1r!Dd+WweZ__xY^dAiKSEp06A|Epz4D)hQye(RdjKW`%)7W8)QvYR`kC0}8 zNVMAM9}IJt@TJ0RrYF5}j{!Loj!s(2p<+rL5QZ21|<{{VhMo^T=EE`WJ=UE~mWnwOgUk%(D1=VgAb2bSb%VarvUC z!Tk#vW`RdnNAz3oHQc*bK5IEVA6dTyN_u)*8hfs_S_D4-9=w76J|EI{>3H(|Yl7l8 zzCx%k8lcBoe(*1Bb1eXH;|=^pG3yoJnD49t;9@eN0AjF>-20$HDEgtek#PZNqWf`C zWdPgOCP1U)+6;hXWn7~`SAGK7->>7l0r10L@Ie7PHI1`VGHnh3~6T!ZL3iaPyE7fc(Ob%+Cq|IA9<$ zm2akk8UPhpr4RUflqUf3SomBa45%RsP!O{G1>hMq1R$)w*#ulfTToc!YA}EYbNy*X zm?)AwY1Bbb2Ts26C-nW-<45A|TjhlS(MQ!|kRu;T6t_9gvSWj%R_)-Un#<%N)a2n3 z_}<=aJ}YN&CA5dLgS?|(dIEX}_r2pwvp_Le8+W2oKZa<#dy^cT>gFP}1l%Bx} zfF5giem%L)1pO;%`t$E0fsn)?joF9pe;k-frzt7nFXY2yBtRV?00>i3TGGyzAAsAh zJ`!H|{j`*mu&NZGPEiB^Rmq3RQ|%-hpxzj(vCN4d0PGHc;$1W#5ErQeP>I4ZoCX+o zXB7af|E<0Ej*4Py*9UiX1{!E`lbf7#uu(#jo806qSu%o>1kt9ufhGzFh$KN#F@uVt z0yar85fl&*6vTiTJ*Z$hQ|dY2ckcY|@6N0{f6S~kw|dd*Vb`vzUAuO@UG+Zi^X>>9 z;7C-W0&!42D5U0CdA>u-sd5F9AO7IHPx*q&A=h|E2Jxmd9kEVsFX?lt^;5~1&-So)2uk&HAV#3H&`n=@DnpvSm0XA1el7tBs!qc#OM%FW*0!m zoR*`b{Mar4L%Au^d=7s?gDwcm2Ch=LR^kD2=QAK4BVos9Jydl;4zc~?(KxHE3;b~p zj3+N*I2WG&J78+0s{f+x)IMNew4%r5wAco5Q6R6dJ)wY-MO`B7QQE(-(z`6Yqze!d zwza@k5G{W&p@9y4!}%Yio-_u?@~g7c7=fv+4h|=O0fcdu9sCp;N8mR(bb;ZwWg6IUM=SvTO3CGu z7;zunwdj!nPAVX_nn}@sf-3Ofx^CeY$C@77mKDZ)8Yg{YAjU53$)Keo5PbT+u@m4) z$(pCFm!}oUm!EZm1WC~w=MrV!&Rk656Ics=9^vx?B%7Y$9psTi9OmIt1;{?_YS49v zp8|pr5f7D^8V$}*gcFMT@Bo>7QS+dmV{7e$#QdRf zO?x1qjN`o^Dn77$udr`WTzvt4>vurrty~iypi#_jcBSzqt78C88ig|zVzh6@_jP(> z+1p|r2)HZ^qz!X?QD_cGD`9$x+0Y;RxYOQ?FV5-%kk0Et#$g2DzTi+%io8B0DazS{ z)K?l-Rc7#;yYc|79)NbUyk=m4Qlk+|YyjW%H!LMS58oHm&jk?|aFSnnMx+P#8#nh4 z((;7Rwl|K2D##D0W=1WR$T(M`yz|&;{Ct_(`#6fan>o9jwj~F3u}|6*gloXF_?fr0 z@Tu}=p22gIvoJT>RS>s~(-amb*~t^Hu7_%1UX!9k;ndfZWT}d<1QTmY7w=Re8X%^v z_Q_=-pqs~KkWzr#eW$Naq%9t50NCc6gu2-nS!7N&dn#Sk_S~HGD9jW!hx6S|1dh$y zJhg95yZH1Zo))=wiSV%}=v8ON8Ifl_zL(bneZ6zqQO3p9psAP+H1KE18hk@>vO}3m zwf(K3bWmhk@3=i1`3-c@I1-G28u2%)9{4CL2;pBY*7Cxyv_(Iev3CA=|2O~k^hO$5 z*Ga?5S=DiTk0vhb6wwM6|AP-%bRf^=fYM-ac)17fc8XWDXCn&obq8Ns0;a##O#bAS zTft4|MT_q07P|s1#}3KiXDVEM?O$(FQ?`VLCLgqv*JxT$<7JQR-B@4>FkORs%mv{q z9A(4G>1vS+&f)kEm(4A04-oo3?i+}^kG)rTf@Qsen+jbJcVGD|USjH6{7jKspd1Lb zIP(IYtx1W~;Nsb#iPrGFpsA3KXMXdSgxezN2X=Ck^*|pSLPW0DK@oP1-oLAaqyx#m z1RckhgThRWtq6r`L9WFFU&j=h*i$#v&CLVZI>~;$0I?q8^cv{&8}M^NuZ5O~Fu6YQ z1ETFdRVD z)`nmj*-F@g>z|NBm)eK$EN2u}rAp+9yt%}MQieL7-C#Y4m2O5q-3`P+d9TrOD$OMX;TmhaJsmX5pu=99UFWQ;hpqcCITnjZ zih<*JvItukatNKE4sg`91F088hgiw;3soYo*QjV6L+@hzE9UlVSvrT70~16P%@^r_ zH;fFkKpai)w#(sxGaIDdJBr*Kt9heHz#~5UxoL znqULb+z!WL!t_ZS>ZtC1zZVP=AX7logAc<{9tmh%6D%7_$37L<_~^rrj8V(U`^z~N z$3E`5c>k;H=BwZ9AxmahfPm{02BZ0oF={&GbJMsVJGVuCuu}*K`$R=RlKsfq(+>_3 z{E2|9Sw_%8r+mmqT4}Wb?sFH8u;rCv0<~81`N%+Qtrs)zP+LtNMyd=&7Dtyza=yN_O)g#f=1q# zL;3o9rItsr&5@;C`xurgy!(Av93{q%5GX(b<7bOV@dQvI3P>}$*2vR=T~=O|emgEr z!;z0V*N6ZRQ!W501ZNl;vl5)PWpE#0`z4|^qpz`nU7ZmB$#^?XkKF`J=oqvJhbo`k ziPH=(eiCWtSpZSC548r`M*7;)fo)YqRmG+X#bEL|3GzTGCEyXjravS08*WXGKB~CKRjL7FeFgXu5wdh0zJz7~RRN;;Yr|uBNmAGon3ayq~4&Bhp4X z)K*mRZ5K&ucQN5Ep3sk-j;U-WA>B82E~?}v-{{U&;?xdPPB`JD@M7yq`4s7v<}E09 zzgr8ZP%(*C@!PsG86q)(%moqvmKZ>!BW)CuV-a6QS(%`W;ESeQ0xP)%wb5v+2(%>+ zvdfxC;D*I5k}xNk^KuE{i_)JVHU5YiF3mbUXoF% zE(^nU;GyZ|O|Nr^>cu;?-KxG|G{3qH?76`8+HINRwr-DDaI0zy;8P@96&)TkLvdmX z&U56DsvMgzt*hk)G=qzy4PQpP3f;eH-)Pu5Bq>R4Ep|SXefxZH{@ss=6*se`Yhw4F z&_TQoP5+$!_E!I=-0;f|?M4A7y6JHcjxdb}3YbI@_lRlt9K?7P>aNMCZjY4vgxyv4 z`@2pQy9F_k7`N*PgX=_qY2EH|bBaUvfZ~U`6EzL5@C>ci*xnezk#gF*WXFe3-5ZnK zLrgD#6!OUtcATjEti$Mq2s72(dtSHjq^;L`-r(2%gD5lKL~f;2PqAd1!wkksJo4qVlfLg>g7SNsbEs$24BE_nIsdK}O5`L)WSCV2 zjbayd00l+pd`7d~c+4T3DIO^l1FUs{C+AdNhgNPEmTffe_Vgi*l>W+sCy6Ad)hZyCZL1nkF)J{Bm=M$c$%)dPrjV)oSyboxZ}^>4f;ctgs%Cc3_vbCV%6pS%4-PGR-q50(Mj$oC&IZ^r+Eg@jMZv2i7QsrO(Py3m zVMD_KkYZ#>m?&5xYGZ1LHBev9@z5^;m)X*=jXfrUx0R7>`DgSb#GYmmZK5{xgo&fY zTi1fHuI{iK(u@V+0q?g|BzFvpaV> zRF^G16o)Md>+&(wf+6#DP~KikY_g6$E|jVF8bJ+5?%Y1cR#2hz&%1-x!%(SoP%;3fVxSS4<<_7z!YeV-qb=CgVjzBz4{mo~+lZdyXxfd$>@u3*jD;Jm`~`In8i z?9bn%578S3=K4~Ce0D9P-puWyFDo;ajhV}q&|EL8=J3mfgUwd8ud(nCF5ADjnu?S5 zQ(K^hQ_d9QgwZ!hOq@sV$%fvILfx6WpV=I}p6-LAL0DG8#*}>pUrP_?%$$#LFw6vK zJRjPO4?Qq`WLHtlx$8LNQUJ7L7C0?yqUd$f!cOR+_gK{CaCW-66_@@Ht^n4DVcQqc zrw@)Z*YmnI-f6bL45*!|J{K(^Q{56MGPEar2s@ehaiQl$z?&~|rC;f&@Rnu`7Cnxz z5{L(KAyqsDl(6y@Cm%m;d52N7OmZj-v7EN?CC52#++bnCS#isJiq+n678$L% z2LIM`urxh^df~D;ETd~2EST3*Z5eh`IB{AY-Ku3_H=|-4!4_VmvplpPycwB)^Ln%( ze<6tgk`#7gzve8a^F{9Rjd-&XGTk}Y>*Lc%D96KYQGS3J#DgEGsNBC_uThzWQ&s2L26DqZtjtGsUuE8v& zgzOgs?3-3Ex!JBSk>e~{+O5>CI9l`Gv| ziAP;q4%_ z>PA)@2YdD#cS#(!)%;?DMN1pGs$3sxM)PFaNfTW2Gb`wC>yxQ@<@Y`kBivWeOco&Ej4(5ks^uIjLTEsNUe@a>lvd*A-yh(y;CrsW`0h)<>tFER-Pq=HIVo zJC#x&@+PYyi|i4}q@re_=hOK``V!neil9wtf2J+sgY9gX`0C0LJ>%sL+KF4QuO41= zkT$ZbBwI6M8@sEMItGj0yMBK)niT)=*t_L%Og%3hSh4gLbCr8vESNs&3M{C|HZB~^ zJp47Ti7VwzS*hLj()gMT$CA$x+v)D%LrNBlj0JMQ<2^w}#NqIPjIA$wFTLxT+d9gg zSnSPT8fTSfRkLkTo6*xh+P+k(b7p#rtf8j1^OI!7&2dIL=G=*138xzaE zdp%vV)VQ<$aw!XX_(clwB&e1oYWG}oT928xhx(CDR4Sr}A;*qm4{T%D*A^^8>g^ur zFZPh~o#Y=QP=)~a7?sZD$6PSDj+4q=7N-MD{+Zegi~XPdj)U^}4F)w8yR(YRtr&M} z3&!G0%dL!v_s20we$$RttI~H>I?cbRVfO4bO?b@qs;e!h-r%^;G%usQ;Mu$6#SffH zE#D+uuAW;2Ly;6F9uxl*&&dIfB(QdS=%6h-uFZ{TVM>dgS#cOd}wKyj?; zSV0bvZ8;61_l;MUA66?$W*HCxq#m@F1HzXy#x(g+eaQ(j?J{t2)RC4o?c+|2;#RjJ#=r{p;j zC4=zeQ}H4Ppy<5%s>K4T0Zm!g?4&17Up#Yy@qrTxy-g;@WWJLW3l8AO^6nh_rnQ|T zcitBuz`z~kA!LBprc)}7&%bbgd4l|6{#&%vJfQpIe$Tsaviw8uza7xI^T&5tH_qq* z3*Y(A9nnvGvYI*q48P`v+CDjV0DEGrh@(CN#U@|9s_0{ZUH%S$AjCO4`>m1ZqeiIo zH;bbS2?uJ|b14AovH$Snmck!0IfO*OCIve0FWPbPgLp|fE0jZ zyk_>WGkxJ&jOz>c_@NJAK}b-SfA}81M+RKBk%$%-{4-Rl-NawsDi9% zAiB6yL}7fTXu%?k zHJ&mb%X3q!L z4|u-aQvWKwUO;kGP6}Z z8M|pIp}4@(&dr8f8%uXB7gcc9L8%)P>79)@(BN~7*F^tZZ=>hz&Y0(?EZV&J4tfQY zx<7LM^O%VC5wF=s&zE`b$Gx4qZgL;_-uY*J0`B;~vW-3E zeX`@;&6&XG^OmPYMlSmXya~SL5^+Z4ZLja__mJ1<+@om$eSZG-K_`t0JAAaSxp9At zNXV-=>*IRUKOimeWas{@Dfv7856q(9Kvf-|^6v%&y2QS*J*HO^J{fRNm_qqqL(Ttl zovMjqjf+Z^W0c3G$P2+bwa!~;8MmjRH~bem)zaLT-rT=*s*a(_j(7fp$+uBHQ||(s z{H~xR-%Wp;eC~3Y&#=jlG3pi-<1P4Q@}ny6m;UB~f17;o;)oxZEms7YocD`EkCl9>;rr@_h#?H9AY>m5UG})2(prmtm(xvD(gFt43*5Np4pUVg` zi1!pL2Vf6B0l^DEHk%HlKc)|e_N?81;QIred&c+&Zhi`%$(0AYm86+n_PvTgw)RxR zv6)(y(C!EqZDiRKaqd25q)XxVy>#p~F96LXIW9K&vjJ)g9iZ!f%W3nwY@*&{*Jmck=|qfL{MC~%;7(Yy4%wmEfo8A)E#lo8T>c|WQEIt z`F^XvqwcseCpn;K44{Ky`J+gnNZE}jc#bqmw)erA#EXG%1=|`tmeQ^SzZ0vUe7l}= zHDrY_Gw>XlHxTw-FiuJQ8^~UIkviwjcx01OgbL|FIzB=5}my&xblaUqMbAx|wJx8YA{7>ed>?fJMXZDR8zJ>a}KJyCe^y2%Ux+4RBLX-rL zLIh>SMhI=09Ip9CUpq^_pr>FNWLv#sDe+n-^tYp}-`#5dK2V|PSk2Kh^!T!=!z)nC z_B|{7Y~TB;xK%4U^h(-V?Zq09x>ivJh+9(Hfpl~o3WFrU`dDo4G8{|=0-ycj?PD5M z2?3IM93Y=Z|IOPcls%Uz4kaAlKN6+du0Tiim;0wLaPV)~|_R!SY=;?bJNU^6}H^;nqXz}1|0n;tLje%Fmf_9e%s^Y$mdN=V-E zJ`_X5DyzI74xl5zc^f)J8*>Aw>B~ZNK9oAcCRFcj3?P=zKIf0Z0SokV0LXiK$d@-2 zP@obUzGlb{l!QXNqv@DFS0`9v>T*OXVGQ8&H<6y1rCGM?w2|ynP6xoT8xo#@*Cx%2 z$StH)MLM$fgHg&6v&+WiLuu!8b2*vLfTz`69&oSIK?d)J3koRm{=k9qJN#oLJOxDX z!Ouz48v)g0b8th9eQMGKsbL1!pDx&n(2C&zhbTBj^gCtodMdG*e$ngOb@fx{>EWE$PT*dZsCMmQ;8V!?f%(g86 zT+`Tl?Xf9L2Gx{K{~#A8C)Cef+Q<5S_14UY+2S><|FT4_upwtipVx%18S<_?(BDP)LjWCA|!CdL7?we4~ny`Soae^43|ka18Q+mGCpy?l&nIi z$Bgc{3AWiF&1%KrxUQ-hCKKRK`2l{)IE2;>^vNS2%XXaR?eAg$ut1rS1L%AJOJa@} zvO5x<^$qjbPmMohJs(kLdd-025Zo0rbF{r1IEjK7Ui#uCN z4?R>pJ|6@pcmoIudKk)AmQ-|*i@oiapImd&ez2S-}w-~<9A88{x- z#a6|Q$2m$0UH4xDu#Y4j5PsYpSUlYjn;)k44r{+FY94ig|}UwlHwn9ukzFtV06Ph9RG1fS{$ejzwZQj-qB?yHTs}n+=1>Ul1P6e%#O8WQMmS zy<^Cl(4kCKI70&>;;Cl7ZrKGpnr@(iu$?maB7h_TNx9v&+~vtHzNo+xYG@o$XnZuG z)&^eY_@1Cc;S!X~9oQcUu^5M8wSMakY_69N9V|%f^Q%pH+`YLQ+9hnjuHqJ8hhb10 z8|=b^*j$4C)s|0KE9op;XJjtFo36^q&_{sOKW8)83f-$c!E-f1R)G-8)L3DYcjXuQ zA&48A*7Q+}H*&`!)kAey@6FEO%v(uNH5s1c07C~GO@;yHZn3tXp8h6Q{ocH?9oFP0 zB3ys4EdvY5tzs}yYFYcBYeIs&D1~K7D~DY#q~}~=N|=l(q8xW3yCgwvJ{`AHs7ssS zkdRN+K*bs`aPcQ=4Lgapd6hSyHcUy(&is08&A=h8$Bz&zq?4KbIE7}}gCnq>?ifK8 z?2toH4G$3*5J&Mag#^`Gpv$eSDn%rw{SfQMee3TSs)iyQD!OIcm4WX}s|pkyw9zzk zJr~&&Br#62zDW%xLIL(mWrOHA?`Y|2^sl@YKU^Pra2p=Q#q>dR8Rc< z-1-@0s@|z(U~OoK(=?**G#vb%=MOY}zEx+SaDp`p&9ZI0`9y`{Spc4w32LM@$?oqv)K;XjE{#W8lA zM#oVEWAd=npwl!>mO~K2&juY(OV4&^m};EUv;zQk^%enQRwG#l5vN06FjwK&Fs!b# z^$gH#t0wX;1Y;$;fJPcsx{~2k2fI5XIXP-Og)(pL>^e1f835&6RZJs@-=`|i49YuB z+}+~m%Xm*_mCK0`eGT3pD%kXqwH9=pC_w2}k_0Q;C0;OUOK@H>Vk{dbV%EJ5IPf5Aok>*;Jbf5h!z)8Q0 zKfu+Tn%7KGB~n#ubLfv3m~BRkV``v-f+dF4g;Mg6?gi6a`eWYcuoHB4e0!!MGvGce z*`FG8rkdQ3Uf5*7wsBcdL$Ym;aEPYF=sdOWD0;B`v>s}_0csCB6l#cuXEtGP@~l{-3hv(wZ-%d9!aLV+r<5kLjgTP0N$4@W2;dwjr1PCcJ)??K;)ZqF!M zP&>L7jC4q5)6+3HJ3F#vHAkQI!JW~?uRNWP8FQDT7@-9esPWI~By0VMOY%0hP?@FbC#EU| z!p0peFM-Bp_dMAEBE$XgK`AB4S+m0d!AEeDGP7}^9%wn8<^4xqP_R6z8-^`)cV^+p zJPZjR6m1jYk3)Alee-WLAt=TewI}GJ4o@nBPT5_c3s7qo%yAk_=Q%7sstUIf%ad|@A7Hr zGD01UXH$3>atSQIjgll@pw!e|C+huriq12cH|*#`Vpb4S(Z_c)rtQ(QKFsVxC_m-n zcP<4Uz7oiBeK5v)@aF0q4(O=KVnXdjh?~$?7FjfZ8L|YRUMnb%s+;#T&(uWG{f#)H z8#4>kRZVVE?6FebXg?n_Kb|5ooRB1Kv)s*wE?y?D38OtidckI}Zm^)PFkh>7N>|*L zMr9cWKsW>(go)NW!6dlVx2ZF%*U&M6r^a#%)^%QI-~?1H&PK*H=L-~f4N&oP|&uhR*;v3KVS7G(v9U%QFdv>xiy0fO$BSO(ze zkZgAI?#In%%DDJ1@6yC44@acP!m7k@(~l9Sjmzkrb3qNbuGf*OpK(dH%G0O#Vp?l= zskjOVEb-|roi&bN9zWo?Y}M!cpo z9Q@E6-MY28rQ}@haY}ecdFbUGh~>=($YE%$v%pnPcfH^;9b0+YoPU?eV(cYwBqcyp z*5S0ob2WO{wbba%=??ia&ttnQIBCzD?28@*^91InUj%6ZBd zls*$}KMhHeBO*=@y`a4}$?ExV9v09>S9H-=KIYNAOs8@6y}OrHTa^8{UubWOW=TYw zR|(htKhS0TM}M4VE?xG#F7kK@zrLg0JrS)_xK%b@n=?MUJ2mVdLSc5fFzF+>hXaZST<8NKYFMnK=r|(F-*@3Z` zs8tEcRq$fMLoGmS%f(m-@32a99x7{IEpB!z5 z{c${XuQ;^76B(Y1Jui;!#}|*?lO~*)!3_{@;n$gjS0!IB5hiQ}E{%S35E|l2oDyykL>isgn3=4GU$sH_cC;`^ZgXZ5%^c5Rbj<_4Ps11z=9s8Zg z@a+56OFFQ(Ec)io{h`Qb$-Z>xQ3zewc57I>kt)E=z!3~l{qN4d2oE3_dB@C_on^*7 z5WUlxcx>|2rL7&tB16(`#yx%855LD-M+EO}9M8Ct-nh8qe1_4B>_=jE7Pp4@zRcOy zOhLb9g=T!hvusq&Cb}2N=2D}Z{dOr9drvoM6)y?)td}fR_#n%c$VrwJXNhGFWsfhJ zdsja#$84zioj0!dtxMgb8P}A?=lfr2?0Y`Tm)EjfrwY|C-^Zl2zNkxTfCF)M9X|H5 z?$n9axGCj*CtuaQ?P+`UfN!Ydb)|C0*~zSjJ+8;sh%tlv&j#?5w ze3x~(Hz4)g9Zvm_R6m|JuHppCRZiTPOrM8SKRli<*nvsU`7>|vyxH}_rF9vX#78@5 z{LE)xWQzV=%GwN5FgJfhw}0O8<5ADMBQi((U!J9eXXK9#t^Z(FO_IN+h?DAS;E>+zN2&+oqvrXYTdPS{*Ee; zt>BX#^eg#R%0W4#NZqoY-Y@jP##%Q2_B~tNDUpW~BZYdQHwTw5iOt@&42o0Mwe!3E z*f;Ko|9Ha<+vOR_C00tA`^ZL}+Ii`S!5w?t-a|50xty78r z6*ti473b9@C3y5d$zk~~I{0sH!0Pz?yMNKaex}NQa|6FMVo4hsYW_tBXZ*B9sR6&a zfzS_ijj%2Hn;ZD85$k#O$@9dc|22oDdzZA#-x{%BwrC|YYu-Za|CdQsy}c8If7+rb z|IX^D*ueWDKQ&_2FK-3 zjCV*haG&&@?ek(TlWD!aS~D3B#Y$m~Sn1?^7Ho??<~-W>iYxok^zSq@_#+EM^S{#2 z9LjW+`;~^K_z|&2uXL&Qt=XRGwJFPrC;PQs_TKXsb+3N9KPhgpCS7{L-v1$Y^>L>C%*r&vj0f-_5Zch)&DP8*=7;|{15<0004x9-S80f z8$*RKM*PAk0D1`!@^?8v!{wO2FapK`0A3Hc;W`vZ@UL=shmgo$7+wpB{)J(66(shr z`b0?TAN9@t!aw)v@sD!PU-)MmVhXkcEC6^|X3?7r9}{+~|MdaVHvzB;0)TG>073)+ zGB^N)$^k%?0Dw66*I&Y~Er9f007A6^NcslA?hpVPqhZ z#)=7PiLo$VgRyX8RM>X7U-3W7Q==kcV5|#c{5CHSYZ${0Bpg2aA2|FUI5jF0ZYKcN zTa$Nei;a#+C8q!aAEWx=Og-ayu_rYCU4Zy$;imij*W`Y{@J1bTK+F9 z{Hx~w9{l5Y+CPu?uf8MML~RL6OGqXC?9_;@30u>)k+vs?MMROb{?CK>zrEo zgi&FDd~+M1gm?fls0m&J{p-3pbNPUucb>28^WWnh#_;-I-~ZtYu?PMau{}1L^s^YA zl!FwJwk`c9hVO}=Uuf7u@&jR5*AD081gby_=z|Sl6QBTF;0WA+7w`w0K?H~aiC`PZ z06XD1T=GFNC9P)6t=$Oub>6T%A-jEF|0AhHlFL>ZzMaS(A5aRJeXxP_QREFfMX zKEgi^JV;Tb98wEOMp`4?kO9ao$Zg17$RcD7@*wgw@-p%|@*Z*l`5L*7!k~muB$O6v z6N-xRMn#}fP`gkis5(>|>H=yIbq}?OT0#9l^P;8D8t9E^2ecnL8l8d8N7tZ_pgYk+ z=t=Zb^d}4kBZ^VMY`{=4{+KvSHl_sAh&hAdV8$^^n2%U2RvfE=HN(1L!?3B?eC&Sg zN$geZUF=ir8jc4ii_^#1;R0|;I0mi;cO2J?yNi2{`^rV&QsUak{FeNi`Lp=RiL0iE{L6+bl!2!V~A*7IkkhM^R5L2jCXh`U(Fh*EK*g-f>xLEj<@QCn= zh@gm}h>u97NWDmp$bu+RR9VzPG(mKa=vmPz(Jx}sVwPf2VufO-#qNrICQ1@1#3*7B z@eFa2_(hx~ZYv%yULk%_d|m=2p)TPrks;9}F(mO?Qdn}6WQ1gqL6layVl*mdk zO8bHDWayHO4j3nkJe_ng=!SYvHvlwbHdtXf0}sYCCH)v@dGE)lt+5(5cY5 zuJc3JNH;;ZRri6Opq`^1L+`TQ2Yq$@2>p8f2?JgOJA>T@T?X$BH4LK+n+&Iogp6E_ z3XJ-Vz8h~a-e!E-_{9dr4WSzvHryu*lWF7<@~{co#L8s1Nw>-RM&pgC8_#Zhw@GVL z{HEiZmQ7VmqfA>(m(1kN!pvIC7R*WJq2|rz3l?$~VHO819#a%3k(48p=ay=gah4}7 z-&*NfZL{jM`fR(x0gS4@?DY6-{6}0uXt+Rb-CvO*RcfxMP-pD@N9%fIePSgtO zeFtfW2!~@1D~=l+a~ubq@J^mibxx0+)t$FFUv@#bIJ)e0nR8WgO>({H2HYIn_PWi} zRA?!*EAAL~ntQ$bGY>tFogUXcg*-z%k9&Udvhv#F^}t)zJI%Y_hu_ z-vvJ%zZ}0?{u2JN{#^ms0H1&(0UrZx1FHj{1{nqA2TcX51ZM`{*etO*VRKIie@Ix! zxlnYdZ|L#RZ((j>2g5#wJA^lcuS8f!?2C96Nr|kAd=X_9RT=esi`kaSEz8kn(R-s` z#8|}C#JrBRj;)J*A4iQl5cfIWE&gcyp9#JRXA*IVp^2B01e4;E2Di$J=Fv8IY{Ri*$EA+%Ie|ElcH+g!pp#>#Y)*BZ z);isCM)XW^J7`a9f7ub*F?H7IZ2vj(xzp#B&Np-lbrxNKF6_9laxv!OLYIHn_$9|n z{g*dg?!2OXAXVYiDKA_K`=ZfYD^ED5p9v*z8 z_UO!l@j}m{&EoCHo{t|aMJ&B~vi-@or>tkZ&uX5_K0mgszuf)8?#0;4z?V;7CBORm zI{%Hpn})ZlZ_mG@yt}>PxAOG;w)cO2DEUMDkE5%GtNkBoA0K{7{Pgv6(VF;L+d6su e`WN3X&%b7T!+xv%uJ--%52qh_A{aU%y-8C65l~S90TEF{M**b^ zsQd#WDk1`cVt=^zy`Qr8IWNw;GtL<23uKIp%xhh1&EL$jwgxf)Y9v5A%COyr@vtM) z4ZQ8T{WOF)vyFW0wHjRHd2>yC?Yq}v^+gKJ{2a8!%B>|!E&Lts`*j^ttgs4j)NYs$ z)vU1zbiBXzHR))rU67NGI8UBwgF~=WkH5wx+h(T_XPs*- z6)&F%?C=P6dEnpmI;`94w5#s5`A>1Z$HQD7y#4z79Gx0i_KJs>J!{Y>!mT$zQ>b9n zFVbDF(N+H9ctDhU?|Pj6mGl)g0ey*!)=je^F&+;C?j5_ca4ObQzwt%ry;ojpo|o3Y zB|T<@$9o--g#Ge$Ox+M7&lY342XR{nKj?m8={I5LIh#r$u>N?(u#7=OrI~Wt#&(ypN$F>T3LH2ui*WKu8y#0*JQ3 z2oiFefC3OZ6F~r(OeX>;Uosv*w3HCGbJkATBBqpDfXldE14Mul2XJ|BzI#T504yX} z$>0n}3eJ0&N@J3$*QOzE;F+?n(ts=1Dg&_g1`l>7^@SO1$&zTL16!1m2>0l+hlFeu*P3qTA7k(%py8~gBo z@;SU{BS40C3@DI}+1vq$op=N6-ZkD)1ewW2Vpl+j#*XcC1I%}QBJ6G$%T(Gl8*GqS zu6eY1j%mI!RIFrd6%(hgO$jhGj;qeZ@>Pbx1N&aIM96d z@zHi~P}?-sjt+tFW48q1og^Lz&ogP zk!8q!gYUci$&lsl+b3I+M4w=+O6x{B8!~^HjhY{X0jBNn0iMwcCtWNZVk8Tj_QXky z+ID3Bs-Ld8DNDmV6_+1t|23d&Q_}KD_xZp1-28oX63Y&!G*9{TMA2Z)*%CC^50B!s zIx`0VW!Uimp@5B$@Tm{r>=7Ipf|r6hiRr5xX-@o1dhD!z%yphU$zF0UJ9sOkj0$QL ziwKbAjLUs?!+_Uscb5GwF2~$`d^*D;XMTSo?)Z!M&S~BYUHT>dp5>n&hP~otABy-- zKH+Rv1uj_jsa&Pn1r2bfnKc6>@t6hTWsve}wBk^kfjJEp>{4}=*pZ|4co~r#Z$|jU<^e4ERilRX|Ac1`Ng2X9t9ls;uUD^$Qz<1#eKx#>IY% zCQiV=GS<5?ph_X{>IVUvvTGlzPG4%;V1GMbClr7NGR&fdAY9bvyO3R8-M&S>a6lUs zu#t9{Z`H>Y_5iLm!aP7w8^ww)XJw$H@FU>>!PKV(*df(DO346ueIbU1l%9JmJn~W^ zC4~?!@f2D+*4E#Ne6lhuWSW$mAZ!sQ0j1U20T|@=r~=Ca0+nk(OyavvCriUahb%UK zE*fAb+VS1m!MpO0MBW99DT#umEdRk2TTi5?X?wR#qYi0 zWa4n{@H6qj?9xMcPhOvf!3+u2ZYi~VMfN}RFtrbG?F8$b7=tEMD;u212M~m(G5yU) z;Ni1nrKzvvjkE6oXsZV+0iuE$e_EMRtCESh%~`_cLAXuuXAa*0OrsCko7j zsob@iK8I+)JZsRy8!YPYJ zr-CK~tD|Di7Tqw^4%GWV+H15nf1*zo$0@NGxZ7p4TImBYi1i5q%n(wu#|lP-8ZQ9A z0$&;#u%;;SSsTbZOCNK+HYjk*x&MXhM|R(q&P!b`Hx@l_W<=gDTgdJw@iL=A<8RD% zKA!$=X%!@nXx<}HC);e5FMQG`?(7M{>CXU@l9xTLgT+aX}W^6Gr{d8Z$y=7CO| zmuC7Cw6s4;jp!X*T|1Ht`S6>J5?3nS<8gL>AZ{r$R9+jWl2&y7T%e!v6(>=bCG@m& zL@L3HRee+rRtQ+hL)D~G^=yeFE_tEw~c_%-8i9C zFZPseShQHqD}ZP{6U9}@gX$M^r9s%x^bf{@q+ES&Vhx-Q5;+{qIG#9Q;|z8f+yide znf6LjWFW{>cK}e3K;cn4k&6W2GS*MhfXCSJ@8jz7|eV@yF(y1sTLMX$q`N)9wM`^%zy{vuM zL(BuIxQ-vGG_%Rl(-F(cAbb@;q02&j9@uk*nn@dTq(2^1boOZ%z1f+1e*VmC&c~#v ziv7jI{e!IL$v*j<7>^D=nDZdJvIDW2e4%;>O+c~>kf03phQXuyA2xqBMl1RQj(ApAAIo_f8i~>sS?k2vu%uHRW3M zvdxJ&E*a#!GbgNp(9!0? zKkwZ}-b0b4N+*~BVAs`V+J1Xs@Ssie&DnDXyMDY0=qnl&y$>AX^^D{3#iE@4#yu=-hGrD9%0kUHnx^c}OSmbu{_Q+0$+=-GJvKAAtO@q3C@- zvGw}R!?MMqIQGzN2S2wz?{AzTAM)OfeSE>jeoq$EP`!O#IeeOaa- z<+4TY+cFUa(JVJIQH)4fpo_5SIR8p~b)OwyK`e=~WI_jme$lUdgz+n90LWo(8DjOp zGEoC6X{m@OjZA^-LVa|_J~y0q460vlw-#Xga(Czso*{H@TJ{7*8MAtXLIwKpD>#d6 z)*TPh&IumnK%TZRUIK(S;tWYS*<>f(H0-jJavncdnP93Q|5TlTm9IeC?dph5B2-#% zst<2^70W()g$4j+MacLR3_8n~;?8*$M`$bI_YonIAp*)x#Hlutas^368c%@WeQ=^s zyvVe=Xt5p%%*TzXBe{U+bhN^ByqHRI+{bG6tM5<&Va&T>Qju5KQ~BQXsjNvyiveM# z{!L-xkYqh#-<$^*=EX?m6r1LhAaRL1xD)H)0@Ei1BWH=bfuyN1JOd&`$`dMjEuEgm zd{aaQ$|^J6s-6bLsqC^$1#|9JD?!zz+BHS%b%b}xEh;9uT!N~q>Z(!Ns(1BOzmSz9 zghgAPLy6)d^#USPl6{IB591F2mIPl%Nhpg*Ey2OJyq6jaA6bS178%NWh(ZIxT#Df1 zF~NbrSS^qs+f0mZ0CB+*RwQnx3->)zAWyT@`>2lf2ElIN*73{?T@A_==m)jGP!<@q zW}E_LH|B%0dJy{##OsnVsXh_`Xi`vaJnIdBMB`Ih1hQFFs4QwJG`&YGzwOl8NoCA- zb6oazr2y^lt>ca2cEdkNK5kk=Z=bSZznBR5NM#BSn!5Cv>=b38Xj|9F3(*OMKmZgC zBlJj8Y}i-Ne%~7~i5}699y7(p9C7?$`enz=ffg_APR7E7N}0*CX#4g=N-3UAjgU)1 z8kEW#B7>b&VFUwDXAY#ZkDM?)D0Jlr2czRO%^)+iBLe*+wYaLbX2L7)>$}Rci3wsMc=c$mMO(;=&Pab8>{WxrSH4Lt>&Z6t|YDw<=Y|T zlzW$=-%4rPITX}(pot^^W|>3e3Q~G`2mn#rf{yg3dK*$(MyRV|B448dW6zKX;@H!{ z*j2GN;{wQ*7DSyg0CYr_ny9@fCPG$p&WK3t{Y3*T&&RVnn`~Gg27OHk>?aD&r|!^? z1*M7x&BuhsmIbH81ow}HunC34-q7P|6^*@zNr7PIEX~?4ndTM(hz>5al~PX<_P!{? zNM%=PCw`HVrxdAtRZ{Ev9^w)g_Vvz5l4^MDgYZ=@xV#yTVlhs3JZ&IgOa~n4J^-0a zvRA>hX`Nu5i?N=Ixp*uV${*_*B=35fq!|@QjVvsik}0gSvHOQ5!a<;+r|;7NDgC5f zUo?6S7gbLX@$R)Gl(LKmF;Qoy>d0(%Hl(*a7;E8I> zyP90*e9&L+iAP?PBh|_=51z)={xXd@LQHAKnsl&1#6tR|;ABP#(TFt*9-}VEa*;RU zo(M2trbW8a>WNVVNK~w%AD??h_pyuv?@TSfV;V-HvF1=8k_cH`dnK0FvGHwybXq-@ zxlg#7{0n*K6=|gxhv;DG<4;W$M)hHppT-_MnjI4i8; ztmmJd*Q`2lz2a0Ela}ispZC$40JBe_@;e^iGEfmo*{0D@5Z-jr`>wh7Ma#ZRmnX;( z7DF%cYgQcWS7n~eHo$zTpQxlM*b9K?Qgx`7QU1W zUZ_k(hJeSVrH(T9ZKEmzul}Gk=omMgF>h#4%&b?hU5)eW=jB$`;+(o zjii|Q!;MJUK&G*A&}~vyc0~ajfs&8eD$`BnzxE=IyI;5kQjAkJ$1`_s zu+rz*$N)iEE8>YSO1M+Nf9vu#<6}VtR4be$kl6Gi6$oUKe&hgwg6s05e~Z+w_xOZi z@87T!ztIV|pLuvw=ibep$1NH~Emc!zJj7c*eJb#gJQl63sP0D8vM&=LDSOnl3S7T2 zf1$Of0|?x|xzyKU>w3A9zdo|*3RJ-!g13)&EZOzA>dZ9hrFR#_x~l}<^-`$hBDa#R z7{Mc&UrVCmYr@F{z(}US@nXR*ThL1TO+*=>@yP(J0RYxmRHW zHTrUb^$M@f2G*oqIh*y!)1CQcCh7C1fYKmm!*PW;;sgBM!C!8qS3T@rk9=SCv;P_y ztzIX*{MhzgcPLS)yPm{Bbm2>#hiOmC_EwMR#fsq*2hU#b`I$l!{P1*@I9dz2=D~ex zw4eg29JPATPZIbWvcyfvd`}NPeGE^k*|}O%mT{JO_P70ow7RIyxtUB$*@0W{bo__rt z<<}kwWR|c(xH&#>+fKIX zEQy(dWAXnSs4w5YeS1JMYVkKBL#4{P2`lUM#x7rnxBW1fBdcf+_h!? z^ZqtGbHpFGZ?`7i>f#Ttn)p!Z`7rj2X~XndSOjdUutk{$xvw7}+~#rnL|X*|UOW*r z4^%76Y<%8UU-$3K-D`^n{>)Vqpp+OA)_kRV@rh(ZJ-N#vaOaA@YWon z(Imz;CsMi81OT-RvhUZ#hBt0!6ln;H$a8>L3#@t9=s>`5)Cm8v_tR?4w+`tvvl(@E z$8Z13JLmn&>-`t+T-~HjM1{Oh zG9Q^U0x%z`Mt}qZd#P7wkW{_%1h9Ur{su+~un$(E0ZZSg6@W=dt!;~cEo-U%mf>ogZ>?ddwNekr8p-|Fux3-`a(-{Os= z#+{zLU&0Seo}LPSq!ua#W;*4)u1C$|%7Pk?b@LPWK`PjAmI-Zs{mSR2;!tXfy zJ`PE(T(lAK1gMGE=a;oQKj!|!w{BDv*xZ%ecuLkKyEpgaVpU`Ip!sot?&s5aH3(ON za1}lEiv@f|o8M{`wBe$6?mm^Jq>6bibBS2%g$a%9;tRx|?AF|oANzuTu0HXl=Cl6u zJt0oxslA$1M5=jUWw|+kqNeQ?-({cCq9Gu{@-*xwt)3@s0!j`EX5PhSzofmdVkRc9 zLkTZsc-xvy5(uA4^EV=KdBy0}U-j6azF?ybs`1oa_Js|#DofLPPQ z%D$BD+=W74ZYE7C(}f%=G7%&}xMgWaL0kLllu^$xaeX#ogEr*yDGr0x9BkdFo2z%U zQl@dNK!4NwFWHWhSqktvRi11Eo2e0Pait`PxSGPyWH}pGIBjFTs4w{zR#)nf?_l9>E&cpullhYlg(hVGiLArpJ`GTq z+a*BMY6#P#}}`tc{I!( zX?peR%Kc7FiQR@8>C=%I?QtM!v~ekYJ0dDDHoe}w>u9<<#MMWTm68b4CZnYN>VMfk`#$(w@;b3m?OD}O z`xP68;PHv%lQWez&ixXBwb zK$2c3YuAl7X4_QR8a>ItT`P;sH@k5>uBq?&m5BC+E~(1^Y1hX^ z1>#6$2UiL;1hYq0W^(A!&UjnR+_(C?bX)H$8Qf0n=*e>%lB9Z3h{aYCPM$B5!nWXj z-L4#~UY>aoTgFfByDF@UJ#i`-Iat-E&q^a_Q;(8!WLQ?HL>dTSNdW*_D+Q%N*0hEf z0KdVu1%>;EDd5s#Jy$|iB!Jep;dv&zfv$0Em5V}zY1IRjqWrJFo;J3^!`Suk_ z=}JLp8t}Cghbg7g09=ERVf^N;8XY9@r0+>VQfES7e4x{3_`<@xzVDNFQEcz@VzG*f0^X1TvpgF$+eqXCoKzUeaq~a$*xLRhhAsA5|{7Yg^ zr3cj?!Oi7;pPa%V7Mdv?_@$v8vbO;q*4-m}U-;&%U6lRjXP*@@`yd_@fR<2h-r2%WO2~L{eCSxBwACk}n_aP6E3Y0j#eRls zw|z-vRoR4`X?02x_Iz%+kSKC}M)+=5+VZmTgKV&sG^_M2_F?5_>{;b_8lXWZkzmsA&>xxjDE#xumSs zal3bu(`K{atC+0|;Fv$SlTR!t{vPJ{{gV$-;o$RgAbaifb^)6vCwj*s|7YW{&_^{A ztDYu-2&)A`{cM23_~p~nxuVrY7(_6?e-$?W14a5~8M(Sa{`Sls*_wphPC+SUpu*3i zTJuobMX1|O7InS2O$U}fmR;CW7sfB#`X)0&rwy(58_9x`n3k59x2(O*^(mX9_RK{C zgO}rjigYbR(4RXiTLAJ0Lfin5OuoF^(s#lM@0?`knNBS@Gttn5K{rTmzhvJk!%TkU*xnrv z^_pIL6c+Hq)*$j{!*w2$Ulh*qyvgqC=%B!v)69 z_(*cA{d{lGB_a{uMcRIg>_P}Ca6%+Aq3tiTU#n1GJ??N)M;~5<&@DHF(S2S6gd$d@ zfZz6ghyk}75E+)IXShEyEFl~4pbmbDfuoIzR0Kz;(GaH(hfv@W3#~ZGfn>PO&J$`! zjMNtc5HX`pv36T=Wg`p3l=xu(ueRj&seVc2ZDPcpC8mH4bAmf``RC^rBHHT5q?@K( zJ_d4{n*oq?DF#Mm?o;cJ;&#IBJ-A<G#AT1Yk~|E{@_=*?w+dQthq9dh4U&T1&Y?4|huRM{>Z>)Cs}e zPf(Rh-0Q4Z6<5NHyZYT@>R-GyRQ)xgf;H}*lAcCero6YU^TO1zoQAlZ5NrGgD#1nm zBvgCk>3c!x&=rNXWO+y=hMkWyK*ItYXX%5T2Aj+lHPU_Cf+h@RT1S^kkA@W)K@T9* zmGj>n<1MZdvy%WEf40RyNG-sm&_r(wi#gWmkRduH>E<(cbcyV`Q9YK9kM&fCaWhNu zqWt>YbNan>dxYB6!m8QHbn%j-zHyBKib~ezd^j$f}>U6>K5ff-Ac4Po-uu z!d%_~7#cuPmF4>X5l-baR&zDJRcsdRYZ86Z63T-6oxeawxq9 zP0HbNMwvZ-Zv8yUV22!5BCfH|Znw{6ua2{i;kWM;vVSM4DK;n~p}<#ndLhLP z_bHp7GGwLLI|yP~Y6p&xJD3>&VaKC*z1U?1#gp@uSuNWXUQl2^TfFR3DSsTEL%YtM1oHRWGBK%}Y7gd64%# z%5Hh$I#`^$B{lGXhcG>3Ml3*Q)vDQe8=zYpFy3)WP^>a7lPz$TgF2>5Q?yO3`Pmg> z7<)P~HU`3z2py*cX=DZ6TB5Xla4z!#IPYN2tL6sWCtYMu_FopJJmP!0#BECDro_4W z{9-zl%}##pE2}`sbT5Ol+X3T4UhFbsNSm!^wWp8+= z)lX?io{a5L{5mcPvBNrtb31RV7l;8c&ojDDa-d`>o^iD*$9#xSs_ufG7QM?Ew(F^ck9lW33wWVuuB1F+QbN@O(Gk zdmKPMawy9u(PWrN$6-M44nq!dvCCrRKpxU10I6Rjj&Rh5TXXD1sew!w|SWJ4clI;3^FH1G~+m~~3KhLeQa>T=FGze|sXi5xcrt6;I zz3=B1AX%(KS#I%wk2`BHg}b0u1S>VjxJxNtC#8~?QXPD$d*Kn`WN+Iv&vbaC&+aUtRwp;j}k zA}(#AEFE?sn=d%K;xt4niq_3p6u)J@AOQG8Qi(TH9)K4May+={?cc`>ebfoxGQJ367;XwzUR{X z&Hk5i)&2vv%2}b(y2)_^+xRqm8pt>DM_@K$;(_Lcer(kxmpks+E+0F8vYRZ1-t*kz z6)b!9A?Crc&%D)hTi=r&pV;QZ^Kdd}40!Kw{}AWp&KvRF<;R!X9QD2FBJ2EgKXu4y zY_aZZ?&~|!*JgtE1^)GUw_cq5DkxO`Jh<~^=r_U2SjCu!|AZaj1y6g7Rd&3S##;?t zQ4d*(`XMBI(f0b>$C#f&mtTb4fBiXbOYk|e;K|!B)cYQ&*bA4QJda4INs_$sVgF~+ zUy&;>K7arH_vki^2_pI&i)12i8$tKFCj@ocwL@8})+kC1vaAN%gR1O?;cL2_hC586 z&_QJ}OKxSEir%rRN#CWbL&{#B%!4}(Ck0J*2BU;bBi0f{OgGX^31*REIVE`UIwioh zECN@rkG2{VtTMs!E##drKa_l^f%fzQ4Jq; zZ9WcW=-EcC{XA;B2WO(#MT@f<+U*%*Ql2|G3!2*RWr&(PL|>D&bl4kGwI(|GYuY;Q z!wv17V#F;So%W3#oh4^Kxw<;9Ri*`Dw0mF7brxS9qGYTdep6U$_0C z1V8s!|LQ>ZuNhZ^Jz}rjc(>@%c{kKE_U(f(&u{R72rrkRktnZk#xpU;;{0F69otbK zi$5NB?PKEcZ$n>_PQ1K z&o_Qux4`#1lk|l(Z3)W~zwaiFW&R01LPc-yXZltKBs89^4)~gXq9!n5J@HE54@6p> zO%Uq8TCe}Vw)}s)w){V=*I=H-7yP01;Ma1qM$UF5MY^It4KY3Z{#pLZ>Isr=Th zlP_8P>rTD=pEcVl&p4rf)s}}trPrm_W3kJ;d2bVzFW&kja_rl~{iJ^`e)@fm!FlNm zjZvY%p0>hggPo_w9~a76<;xd8zk1<_V9p!9XX<%xDz@~BRuBFd7p+w)SeN|sox{HD zZRP1>7uR`M{+pHk{|cYt&tDTDznf-4)CB%p4g5cA%V)H1G6(vw!CA9cbGZ0U4bqUj zt@D4afsY>*mn~!h5Pc^Ouaa7@{#ftv(WV!lzWw?yHSn$HcBnV^rC%-;T?s$waE@H~ zziQzBU0b4ja{y?QMWi&m8+S(kw;FhaYg~qDq(}#j9g3L!j~e*@u(G12G`+KT#M$VFHwTAKaKv)XX!0@ z%R@fZ2JC|;1PZ72UK`v0Z$77^HzI5saP02?=JN$^BIekO{7$dxdDL(J;|t~T|BKI% zgcFVPEc)Jy^Z(+LXQDIoW%=gcAc?MG-4KN)`xlkTDlbAXCYMpo|KO(d(r&0 zkmFtg`SBzk0L{M4PXlJ)EI=gqxB*1bGMonKz9B(_m=GSV@VW43`S6uXUr^#zj)<-@ zr2!_|==t3w3x*OOD-9xbf(G}N(oSvbe#V=*a*!{Xv-<7fm!yB1KK-RJ5{4fBXKg8P z!8=^yB1St>!ITWlk#R*18BA%)8URN+aC&$Jab63ceC>|_5pv-pz-wGF4xj}1f^E*K z60pH%OEs96AO7C{05sqecv5Q11(nH8`3sSZ`wH3o2NS|g*M9&&^;Cjs7rciiRUJ-=2y8S+>5Jlw zH!}PXl$QrE-pLIV7~WwK_}6U2EfFqC1-{&=V?kM$-$60~+F)TD@R?~nvf(9zaqL~@ z^eCZ`wqL2Eqmgp?)#fT*{sMrj9nRHjuR4Z(h{@nGnsPeEZ!$HTL^PXrx+HBeJ^N7E zYR2icw$04!4>-KeL$gsr;)G^MrzaAmAC0};emCzq_to$kecF9PoO0DM#MIsh-8t}~~Sfs2sm zi^G%4y;$j1AQHIP4lqnBYQTfW04nITY=8r`3J!QGXlBl+;X5soP2;;Pd!a6&^0wc& zgnQ{}fb`x^d=DY`2)?bOAEUit@7s<(oLJ|d1ORRL*z+4D#}RuW;ZcGu!zdrYM3e$T zC<7_L!^HjcsIbobWBo<9R^+ev?MRc#U<2BZ8;Uz8$#(*XgK zk#p@2IF)TO(ebbKr}Wb}cBejSJSW>yRgj5GDUYjLYjH+T{nlZM&9DQI1tX+54c@Iz zK`_>Y(k7G927Jb-6*2Mj$;()S3TKuTKY4qW>>6x+x!Kc@c_YzUPvkbh4odX`rUQ^x z3#5h#hwFyj2~-qS#JKL3`l^&irCmJJsNI18Dg16`-4Rn8LbldQtMJWT)cBB;D~b** zISkZJ=4NN$pl$$$*7|^DUZGAh8}gb2NcMXLa%Z6?h2HjuW)U6%NV9C#ix8uFm;r_~ zEXovMcN%#!2Myu7C($m)$<$p7pmIcQYAM%*0Y7m_t{=4%O=as%EU1MZy)^Xl9= z(oh9igLW_7XJybDhcEOP6$mg6LLK2PwAeaIECG0TIvX5DO z@Rz?f`>4WEz~Ab_HTA~XGT7@H?GN)p>o9O}7aCHuQA%ek5XgwQAoz+DczV@^WM zJOEM=S<-JNeSbhzs6g-lO4tNQ@Dw9s1_DV7*5)e};n@Tgjx8(mbcO+Wv7&{|zQ%L{ zispb8hH4LWe}gg&%kpuHM6m8VTy^vE|p8Drnf8)KGrdQ?nX9B+PBA z3WC!}_sJ(X2C!oJ$VMS>-&C;a%(xJ~fyUK$5o)`oJ zxM1~bJbiNwLcThEOZBYUl1j?fU$xRx3jMJ3Lknls3dzUz6Vn%Ig!ArlY-(L~qvMM) zT&sCSry%FpZv7$Ht%@6tR#5;|s=2RJ244%)@uy$zyJwU#$P^$xRMNp;f8^a8)}|OU zp@j#ok)MwPz&zq*jZEltSbW9I{DDRe^Sl(O9{?bpaHG(+Do=98sL_8(X#Rh zo5NrL&6QE7RGU#abnhMkNTv}KMl%~SF{HXxG-VH#dH{G0o=x^FCDiUoWK1rJ7zJIz z)$ax5O)VWY3&yhBp6?`QW>c67b{-d{uA39QYwT>%z0Ai%S$R|TBx&G}V%~D1i_fh4 z3x-^u%t}Zu^{Lf)mA5UUN)W)+Y?%>$2fbmOjV+e~P%?A;W*veJYUM@)PuXa7%tF3w z7Bo;R3|EGi?yV$LX3Npg? zb>h9a&qC`IXvKLM?@{6Keh%H?-~i^72cBpp9X^)sX0{e4fF|qlaVRyhHA$s&LNY}s zXz{qhzj-DpQ6sHiacWyN^8sh2jFlGe)Lh3hEk9yU+%R*j>+jXJYgU&pK?Se`m$@hu zE=m@^tXaMOJU2#Id*nU@flury`AmScWeRb?(j^wMjyc@2{zn(XV7j7(f#20M6@Tr1 z(PN@V8E7GVmg>E!2r(PaV=0XNw9Nl>HP=9Z z&qxY@-HF?sz(PshT;N~Xads~JdM=?BCX~0%!npi;rC)tZ9L3za@u3r>Dtx?3`uKKQ zFuN5M)S+GrPC{6CC>Hrr%>cnepfjV5W4Y30*onst6%YPSg2Oco2}?lwrLb)$e=R@= zOBnE`#3~aIu%~ns&5H@j;qARD1ZQxAWp#+(bx!;)=d0atw%t~=Qr>Vo)1)qwmTvf# z60MLuIG&Z3A4b=M`YM^PEaCG}!jywkKn8|r4N%My;JPYc-y~3UQ()?jAlF?%`+I_^ z76lsuL zwF+dSZ)T&(DOeJL&Wf(f!NVXAS?Hi2fPSO<{HvUjQ$%Eb@8dQZQm(wH@=4ixF&R>$ zO#K}M%>l1+TMWAJeZ{7OAkMWNBd4??=fkH;>E@rN^7Z9hgjDdqWeE1Q=UKK8x!iW; z9*XYfju!=r7EcmFZ@-l9dRfm`N%cV)pVg4bMbZ6khm8o{wN>YKRCjb$FY#2LIibPr zt6^|na5WH5qTu}yA8aa^PuGaa`EA%VKmM%B<rmg*6%B3o)9%g*hd4oQ?;Cem?LraC=0j-+`kZk@pagzDc ziYh5`R=Rpo_lORKuf^sKXNHN2jWTJ&tJ34}-Br6VHCif8c6T4+@cn42y4)=OqmQJ) z?mv`FHZkUP6-u-WH5@pcQ=}fYG%_N6qn~PTZ$Wt~sa8m`%jqwOVJKXA%4^N>p^kE} zu4hj3hq2M-#NNKT9n?jn7w0(y=&=o&$9G^<*24c zy}!olQB5(3uJ<0(ja;-D{aE8=VgOwv<(U-`ifO?UBJnbjq?71>j4?lmH|m{j-W>h% z&{R>3xYEK+9$umt1mE<~V{q#--#_|X5n{(z1+>`uGyueb_3a^PvHTPtUY2o@&;TT( zqK+&pGt2Q~WRp4M7rM6hFTsR~31u=+v9+ZrrS{uEb>E=yBuWjUTWkQ(Jh|Hf1hTUu z0dU9YkcbQC9Vi zW4F7Vt<=tQ0}pH9ljYjj_L&zEp9HlW3N#)&T%zafZ`_xMMqr*xDjG%iftfH-(N%)G za?6M15{^{`W2${eS!R2Q#APCcWD{&$1rR^1Hy%J@?-(G(uIr1lbXtBr5y0ac;1U>6 zellP_ERZMEisWnMqi=Qf2cSITWdLS!v#Vtbih^AtB9NgsNXD7mpnQ&KqUG>D#$i*V zK!s3TCS6=Z?5sUczT)A7;_$Y(OlW|?e0BT2Ldgw>8zP?IcqU`o3kKjK#4d!^6DX_5 z&}DvaEc0-Qr}da=nY5czg^PEn2LrrF#Q^VTyoZ}R(0r*L#a=#sxZx6hYDJx24DAa( zt;t99e!ou!sLvkf=gvuXJ}{sHn=LNZc~)Osrr}_|uBZ;d7PZ|k5Z))od}Z-RoKnjjicoU*GitGA(d;Qo|AlfsqPOxj>KHsdg*3M zm9-IE)8}x6&B_S_wDL=JXE+UZ!M$QLqw}$-5w}h`f zMU{(rJhgabxPy+;4hzwdvtYm?=o_j}8|Qxv*U28Jbu^vNFx|~Gy}~km!8QY~9_Zs8 zSg#xS=0KqDs6F{9cV@K~y5r4c4{4sH4(|JuKZycmQPBc~^X2Rmyga!TvEFLFGAm%2 z%|@Q_8@t7Rw~IhSBSU_1GycTU^wMNi!uzP`+GRybl5l}Mne3&CO>%I~&{1^QkFz7t zBtGrhu#>Bj2fv61V!7!X@_lAiy!xTv44}@31@mD6H;wsh!rDeQrU86k%@Sdqz02C% z)j`FqUS;w()ri(We#M=l13(qi&>mR(rClb`e!!cZiV&vzDNyXtg*q^Xu-VEF;pSOf zADSJ}z&pdA=z2f#sUoFI+q2XE*in{-iFJD<$yt$`k(!c{I4;%mBT|KzyS2^KeQ>^hdzm6d!qEKRUD*07Az zJg_}z&-P|>33rauj=Ww+;5dJTXOxdXBfKjxd-r_;9N;RlKYPU`#_el{)Y@657;usM zqm?rWb*}{WO*rVIb>z>E`>D-L^XNDHKlT9_?*b7*Uose<)(l_ zgJjm+Uxc{jOLb|j`lxUJI?lhuZ8>?9^yzh@PVFaV_Cc9g#%%HLC-Z!@&fBr%L zl^_aUC%nfk5ePq(l!~JgNzAv!Qf}u;=aqF4`M(^Ep;hzwscVIZX&+ah_;9VhwlKh* zfxq)p zYkbnGkM+7zLrUJplIL+kS}Y$?-H*P9m0l(0mTOQ~(bmf>48L^)HvGzYNAlB>2Q5VW z+Ffs$eBTE5fB{p$N{E<;J?TT$^*JA@7l2VSI|!TXjC&2u>Qk?Y(}tkuwd^#*51dAE*Z#1o z(;0&bz`HltX(x}6pK+oNpLdb8lv8Zi%4Y1Y&Ro*Fi|;%e{Bo>~_l)%63%Z@>w;3A3 zJi979cq5myk13ZH%T&rda9s7xam_cEb>A!>T@yBtI;$mK|6d*Yy>SiR|KmmtBN29g zG|2R~qKQD>NI|ljln&3RNCt%QDc@(w&{*}} zn^m9f_#@(n*C7A`mIXj?ZWFN&;f-ULRe1-t*x;8kpiyVm0zCHHXu@F$wPJ!=$kY`L zUf*_~pU~F>MBTY_?A+yJnz-~-7OlR+9~%=f2dzNe_=*9@6!KZ?ajlXZ2$lN^P1h*#yD+%AHU2Y0=S)U2bImY` zJFG)LLnaMRcjVTph}lx=xEC3f7gzA9iJ9^}VSt+!a~Nasr%{8?-d*UwGxVeXVboBD z5MWIQ^S0kbw+1UW2Ea))hf1nroyPtPT*`QUIxuhhVa_-9=nt~_pFN(xGxUEM3tZ)G ztOZNb_Kw=DQ48p6Q+r|TguEtmP9* z6CdB(UwzWICc%sDUn|$-9$c5$D`avw^2_%-AAjuo53)Jo(DmCL<+(?cLNj~uo7CQ-SKlfRbFy{@ z2yibPe{s3x-EUG4y59d*`TqU#PLupwD<@xS-uwJhdhhh+&kNxP)^mLk z{Vx4r^XKmuUb2q9JnMg&jsm#@L3*jwS%BI|q&Ycp10V;zvuMn5uGJ%hMX4o0P6XKl zKm-AU^r13tnG%5A;zk2-1|c~5dtkD5s?&WjbFT>Q(YwFzm_TJQ^#Dj?+yjuGTq+9~ z(slR&VjbTY#Ix68%T8G~s$>A}V6WW~Zuo`)%)hXk8IQJRtuk^ax`eT;gQ#TUK)-eL zAI5?l=leUXJoobCG#kqiwtk2CKIp;VFpgFWxH<%R2A69ALRlWc&vlOQa38V?wu2!j zQPY?RFi)4vbGvDg#!3(Z;B<1{PRTlp#v+$i58Eexj~4r&4dZ9l&1mdHqc}NZ6HOUKr+szpT znzac`+?R3O4vHNtgmIdHOgxoffgEeW4H6LJGKDdROdg@IkI9^g70kl_m6^z+Q9TLzND(Mw=Ed7>GJ zakwZ00!`*i`-=lC>$PE;ELym_&%0G|)X0E8B48F0`7Jd_`k88Pf= zE!LI8>GKqTG7zQBLY7Vdsmp%#`~@YAEg6LR_pSWh6d^bB= z+qlD}sIKuuP-%T*Z}gR8{u}EiPLgS*mM#FP3~{{rjMxP@h?LN6B?ViAP%~tIs9~NV z$39Ur7Jy6?7D>0X$w>Pdk8EAl+qyi~&rv4O+lfJ?Btv$?dqO6mTmyzmR6=q-lZpVU z|9%&0AdSix?|1v+j z)MMgW^s49d{?gYyH=~E{3QF`WG(101wA`f1KyJ_VF2t>B*3KKBQYVycFu1c?=wV84 zitQRTMWaS>=T3X-3ZKd$8aIL<#QNHosjJoRa}WXGn5lRDK>H;n}X z6~`yQdKio}Z3Wu$xE0+@EzqQSUu1btDw@zO_-5Y^0BhkPrZp$TFviieRME^)aU>&w zAjgwX-pg+h|6rR=?D+K5l_aeYy4d!BK=qS^gJC=)Ni*Te1PL@e4xk<<_l2tcEeZ`l zYQLxNq50*mk-kk|@dK#?>zy#>`+3_-FCso#Y=@jH0m8{m<;@h0m*L361g?nvZGcld z>&7UNB9N}ElbHVmwcnVUt=*(QGWtxsCLtvxhlegcYZZh8H2LC2?!R&+w# z?)DDgci$%Vuv0!Ytyy-|&I;K!P^+1!1+5n5)wn(R)nFI1vUsWCiQR5w$B-YWD( zKoNR*PnCtYyioO_m#-3GrEKT9X$FyOP^ z4<6j6l=9rk2PA-CmWChxTt=I)X1nPfgJsh*U|s~1aI!WOQXE|E-+d7C5K*L|Gx{~qP&b1 z{}8+dZeyL~;&DGPwDdm(Zxhs5pT7lfo<9N%(79=0U;ZI@`y1)_r{Jye(7_q?l=El* z*VoaQ^2U!kW9{qzDR}$yb@Zr|&6_f((cMme3*Pz(_Co&&_a#fNgry? zx8wA_z2||z6qCc5p#=~wg_3NfgCq2t1I$0UY2;YA26(0l@7c3Ug#TA#+W*DqmjI9x z2Q|OT{j&a6yLZXq1O?db1&A>A;qOuOhtbdd-;8N0KM#Uk@5z|woj|4A`@fBTe?M@x z(XTrj#cN(Y`PNXe-VJDvyQ|KQ|6%m|^MT8szW)p8ZjzypYj*!;^!w)nkJNsGSG4(@ zbf-(_TUi5V(mx)!$Id^_%j)~r{xbRn?Ebs{xc)m&^qv~)w-O2UzFOM%j@-ELm(dR= z&^9gle!oBXw+H^G(J%A*-1(b4x_Q4DWzB8-%jj1}5br_`o&C@HgD#_J#xEdW_n?9(bL~Xmsx(iQ2v82Cqy$jyL`>Svgc@y{6lv{qOZhhVC}S z`~h=yla8ewKOJ4!4}8p3t$^(GCu1h)Ahc&P%Wt{#{upXq%KYq)U6y|^JZlrqd$#YL z65{@g;n8z`TO^zNFdTJKV^nAtn=#8$*r_|y^#{ZACu1fUll-+q`_Wt5y>6Y~C{Kzb z%8Na|ocd?|5#drYP2KdlHU0Vd6}B-=p(f@p#*AHm@cD+O^yWN!w>fyg|LeU?r@61s z*^C*H2TfyTaW9;EL4CDy^TnqxoM{UJ+`rKmN?O%3*^HT966I|ifA;*L*WzA_$2A=I zf{vTG|HYV1#;^G1)_CK;o_+W&^=L(3kK^$R%VIn!Rm*vi!Nno;`{6eZ3I}AejcN6# z8dl0$TQ0p5yUSZscfwY??(&YvuJ+X|;fo#b#T}=+-k%TK)vDn0NwZB;T=@C*kNBGp z`aYg#pPg%>aje1*)>oIWf0*MH8m_(mQuMFM%Ev#%oCm7=cDx_@RQ}O8d42A8_GBfy z{!sZewp%OXh}~tQM_(j7&1e6rKL}Wtg8$3zNtnt480|KI-zEJ2q{p;-Ii-a($pXSI zRK5SwWB&VM`j;N_Cbrv+5@l1U|2dN0^^raIz5&q80Ih%Qo`2{ugB!o*0{s{= zFFoeJ@18z=`b^#!+W3drYf@8yXAU5q-3a+xk1206eH$3}kip08ng7e~ney%`LA`LN z;P(w6UfBi?&jF4fWU%{U(qBMa@8H)TK(Y-7Vwf!@R#ZT&|?zryy$M?Jb&kU z$LjGfK#ETp#18{#U1-qxPd#SF9Lf>~(&8{kj=j~UUHC6O<{v$j{kfP5BP8nghJ}AG*}HM- zPay7ZVDpc~)S#D9YE!q-V&ncliutxpopWvq)9%rK0Gla+{{-TsAKP-r7eAiy|L0=* z6Nq~^$Yb9T`%~HX@t=CkKY-0Yiupd_FxtVo;_Uf<6!U)qn?xLRTr9@b$TQ|{JP1* z#4pctWAl!PA2d+Q>~90%LMhu3d>j(W!JHTUKSn=^OYi@29LBuuJPJUAJ>ON9pe6O2 z$v%$3Xu)V!coaH4|1gZ*La^)V%RA5)f8DzB{CV1ChuN{^vtO zxmD`*UNc@vv^!`lO1Naez|d6p9w(Sy7P!6frE>8_0x*QkyHD~iaC*}-dib)}i?sk4 z5gXq+O`ynQO_TC);D$sy6Jc`O3y22&2rA{fZ@iC=Is;0?Q6#`aQ|4D9+^g%H0FRz# z7U@n({<66EYt;Jx0|7YEW?a9Q|IFQepRC45JayAe$XwGw4Db+<#V%*!`v97D^SN3Z zqBLp-pfD&HjndR>a^IhOO;-@q`LoAj^WVw?&LjGO``QE*X!D_(CJshHK~>%`RI9@T z$r4CdD#uR0dOG}?@7ipFh0VssQCKopX$^3Jk;~`8CjghD5#UXR*O2cw5$I$x2_xGW z2{;Ap7*K;Xn)}R+awg`a$AYYArpk3A?%$E@nTBZN_{IK)PM{`cN`@$%h5#XRO}t{} zW{& z$a5|*0GIUmq2=H7FW(LRI55$tl~GB8Vw{S(aTM9hres9`*|_pS%r;{IG>k9T$tyoF zX$C|Nem2zRfb@d0dmQ(dFYj=yX-?VIERZp9yTixu2vI@J@Mtmxlq+CJKu!SWUAQFI zoh4sNymkNCotu~N54%sYtHN7+(8SmmAd#A052Q_j4?vMm&@b(A(&Iy$W|S`)-M`ZJ zdDnw0+-w4n2~8Xrd^Oh0o?r1gA<32`Vn#|vAO?`04`Zle$W(AZNWlx#tDAPSdlSHD zDh$Sm);~+-jshYBjq>+f&K+@kvfcS@?eD@)&Zlxip3I!8qCWMgXbgKgbE7?cV9f3Y zXWiHd%aZz;hl&Xj8Ivvn=V3rq$z%a9L^(jjP+)9>j(D$3H9b&dLgvnihob_`{Ap$c zAcb8&!_T;;I#B*#lcAP<9{vWk8Rq*wsQCI?*`DAPymW5~D zmc>;yvj4A^X41jHVh{GVGsLH~P+~hLe7qYcu4bC2akC<%x-(F0yZ2ifOo`u_OqW^R zGy&Mqwic0khsPHgKGtdgvp`7SB^?&%=b2ID zeRxS+-yT7*pHn*^;02|a2qX?lolj(wm9Y)j{%z#cV-)Pkms=*%nVH9>JWHLW6qiB3 zxp)kU*4r7HSx405QG! za|5RyxwX%ncztv=r@ef1go;$^CyCUrck7_ph-uA%oIJ&9G_=zl%UzPHt6ezv+$IP^ z(XY>viCw|0txMzS2t9@e!OXc0NmfpU`Pr+zyXhzhmQUV1fhzR15XzQdN-O%CGOZDl8Rq%{HT5U$22nsH%BLMSfuFF@q1qBKh;w{^&IykPHw{NHw&IK`qI z`Ywm_kFpPia6AAnMV;y<2nN;5&$nF+Wm32R#Fj&{Hc=r_{5+pZp4$<$A>?Txv*8fr zQ>SlIax+?b3@_e+9jnL;MzhuzSwK85WZn8UtNrQ@UIem;SpCV%frFX^wefEo6tmQ6 zl-go%UL`ti$vL$0u;Jy$8gics0rUg$3OL~!3cdF#?_>KF+D|-S6DAyjKzS&;CT7nw z=TEZ6w{iCYVmV036G4UzdD*+LbaGw-0AnwtlX^bbHRf6ob#a0AI{G{%PsE#QqjRFC z09X76AUA#z!w?h9l*!q}PK1#h4F{PMfCC|a02&MtzH+XK%t+=>%=0PQZ7A6CZi+Wi z%Mo9!qqyVwk+wKTho@TSA^}+5bgb_4&pv*^#Lhp&bD}VD+^YBMtHt^F2WOK_jwPDC z6_T%gI8ylV>KrH76(r3VR~#Ve?eo;-CCatkKnD$BT;Ht(2hB#yIyDK{tgW<1>0k*v zIfKWZPiWtxB9NM9rAq0BZedtxP~$)0L83rp!vu>xZnqs>m}q@!%NYNxhizk&dF63$gk(J_ zr`E?mfZ0odtdIsUq@j`;VUo8_3c!n+H>A|0yIT76MGu7yHx=o_*57!V40bb3!*DYC ze9+dUS{M^t@~&>R$KNv3i8#{QH8*#F6n;3(=}G21PR@}J-Znwn0@)VhH`jQ(vE9gs zerv^Rd|maL{pedK;(q0ZS$>VUq3kl%>V#T@kpT1)izY}ZAkLMH2ooE`$Y!2V$;XkS)`jt&B$S7)Wy5!mA_on&DJShK@;&I(a+1R6NdKQoBRQKTL#S_0# z0C%!OwZtC!x&8YyL>L_idrs-%)Iy3 z=KVc2_7%HW+*JAIOU$q{)QLrtLh9W_1sj-pL(9*P7qx+s-vkx%_rydNl z&2Arl?R)m~%;p4*tacl+mfU=vt#`}8@DUS8tG^b3?x-HB$B+S}JR>si_sw(}jn(0R z11!;FNP;fXX*)7+Cvsvp3h}}>S;u#QqUdBMV_`r<83X7(lH}M#TPmP2r5nk3N^!YN zq)-(-fF)H3U@VFK0}Em@Spce*SiXq?41JcW*a0sQh~+_MawH|}&Z8UA0>o+5WR{a2MWJBnH(%LkQCMl)^IExJj8Op&25qRdDrIRJ;&L(G4dU;vR(ViTOYPD({s zExAXLo2~(Tqs6>KA0%3t^hCP?bl?XYqyyXNRequ}BXwlqMiz{1s+5FM=~Zn&=@TcZo-rUS6}CZxF6WGl3Y*MalFk%8Y@h=oHW#wxxOCC^Ug+ zi4De10`kt989qtbAc+A94SbTOMJbt9N;g&Fm}wY@C1wf(s956Q3tY@i01Uej`2F}S zL0XvjsFI$5#r6KxlQdKQ+iG~KnY2tam#czKfm*q%uI`bY`W6-vkem5@H%OeqDg?FA zMjQHb#1+f@YXXq{4|Glah?76kQ#LSX<8GMJM8-%+CQX5vi2U$^v^=GueOX-lD&V^= z&a$ndP>0czvL&bEaVOz+BALG5RV z6{MJb&d4pu1G~fus5{g{3;@Ro?%m>e2xCTeczijV!H%lOn^S zpGXcIlhX2)N~-3_sAPTC7q={8HwjP-2RWwX?kQjT< z5XIpzJd1WpmVA2@M>(p%%tzHUi$Q^kW+-%BJccYES{1fdXVHzMNaf%4$uEiDysRq! zPBo?py{Jemg6wp&?Fw%g6_n$HJ|Up%YE|)K$jH4oc_h=|D= z)z)=2(q(oT3bq>-A8;xTb^7u_BK5FmHP#FGoFRE&P{#uX2o9!0q63{GuVPvmlzn6~ z#{{rj{nbFCLtdtSXdwI2pXJVb#=^O7%^*YH( z0`bn80^##otTzfc3(sq~Rf)SPh_ftFQOOeh$GJ?4({-p91}%y6Cvgrys~!~6a&%`8 zAMSMB+HDz1mYTm5bLEs(G7+M6d6u}xmiVAMjHRL0{icIX9tWLf!u46oVwsPE>jHy6 zRrwlt{qUM+3(<4-8)6MtTMXBe9Na))iHgo|e7X={0m}afM-s&-H&I8i|GxN$i$oPu zh1Mhyx`rVY;~x(p(q2f*tU8kLcII1B{3KzS{L}7HoNSSmz99QP7^$=#H-T4Y>nrjF zPMq3^&oO-=Vknome|uuj;UsN&ER!JS@s>Cwo^$q`XnQ5#tNy^eiQQBc(;DOoLIJ0H zyJDLhO>UL!~;VttFoZ`tDIyj-k5K_z6~zFwUT_V zTtC?h#E1?{J2V)aT8tCGm$5CekAvL1&$M1JXho7_b)9*^u5<_o}2RQD7-NwYC9qU-i# zku2N7;NC8q+CnfJxHAuXBq;LXQvi; zB>K~>Wnv|6sNorx9~plf^=ndMRYz4Fl_wGJr1DEYIj))V8cl)K$W-LQoRsOnN!lu+ zsS*is33;Udqx`@ZAZ@eQDGv|B7Lv|p6l^j!KRu*Ms?qM|aVuvG! zG5^JH;i^b=9|z;+5t-X3zn^+iQI7msRkf>LqyMMAOr$8^&$7e(Xp-0|7DBpDQKZU) z%HqPIETsG4;FG`I`hl35A1BkSMGp4#9>3fByn|GsG}yO$NKZrLnf4W5!$8!_D{roM zdTp!+2`csv-gOeaTVD(O`o~R=jl(&dzN*0V{9w5;kRX>;YKTf5S@hd8KaejHYKZ!b z$4b1P4eC=v3`uvw4f8jyP-;2vTwJo9m8Kd94zHr&^Vyou5Ya41>8g+zI_R{hvze(%w#osWKMPWhd= zlaMyrghS6M3VYmrX>{+SC;S zCKyU>F}cW@4`oGP$%5I#FeJ;T1(Xq|V2Stovryb40C2CWDL@CEKC*cBS_uSKc>?L7 zKl4inh`rO-f(rR$VJ;*#4FN3a)^X6DTrC%!J7^TWTy{wRRgJF#fVL_aqyNe zmx-=oCGPlnLJYDZyuL6Con$Qm)P&P0T=|Np%5&0f1L_y3H)Yv#y_9oh*oGYdC(CxuAz6WI zK)n758^yV}-UozVo87t6vd!n|wbpHAE7x0OIC?#6v0_c{6e*>v?CXS_m(Bnn_7q4n z5FQ)|)}2$t2b}Am3O!Mfi8=%zYB3uo%ID81Kk&JGA2__sl)j3^euW4|y`2aA_Isv2o`G*hf3dboQTp#Alt-{>{Rc#gu3V|KA8lx&_c5o-jeF*H>KWP0ICDo|_q`kOV>HGm1Vob$eV z<`VEpc;DJdCpVcKDz};d1T>gszc}2bPl?pCsPJO^J<%(M0NF{8kWY@QJ_lt((%Zozy%a{Ss0&=%t=NltpcY3((F ziOu7*8QGzyMm>g!upiIYdXf7~jJCauQ(KqyV3FPdWJ)KM6LJEWj3t2l$^J~~!nkgU z{fB+_Soes^1;^&*^IO{37^x@(cb+TY4{@;RF;WWNxKPNS>TGLdqjEs+Vi7+bd8fx! zHtomdJQq0Ou92^LuoslM&ab`G&}V40J;gyNb!h&i_V;7^DV9Fxcx{b^>y&^^7hbN~jiLJo#} zVs%vS9Fhq;?zFdF=ysDN_*z}uKVwzX9L~i6TB+10I{Vk3(V`A(n~{}`_v^Xcp7#3w zFmF{fHY4AKlD{%+eaR5Ma}H7Z^!07hKH3m5@ zEIZK&ZZC4uj6nsg+<}RCEi9M73qUK+zV1sRu?vd}5nw3%Y+XC{ z>`+6O-12hk^gJuS|DeyMKwVBo=lT2lgTe={FM-X`L!7T;bZ$$Q$sWuiCZ}g`!hN%G zo3eE-uCv^BZC})?=e#@T9|fs7DFMh(Ns6HOzT {w7Nqm@|pfHbA4`bM%SH;p4hW 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

    0K$R4gW2BjGr-UfkGu(e>E)LA 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 literal 0 HcmV?d00001 diff --git a/nltk/test/images/twitter_app3.tiff b/nltk/test/images/twitter_app3.tiff new file mode 100644 index 0000000000000000000000000000000000000000..e8f1960d2aa209201c35044f28da8f6c96a51ba5 GIT binary patch literal 102274 zcmeFY`9IYC`~UrX%{~lfu?z+?){!N9lr+|mH55Wah$vf&ih7NGU!ttlkc1GTw2v)I zNK&Vy&aos(MM)~v<$Ry-<@~%q@9T42-|y}F!}Y`U7tHfEkLU4tJ&xmfKkV(nIskE2 z;4OT|1(diDHo?r-sny>=MkK|;&)KkIhlWIk)gI^8>BF1la;*Kg8!4W#SIM^x*nY$R zn)mubyFeGCil_TGlsfEnxiP&QyM^l%}l%Rt4am^fl(f}XO3(>KNNJ(bF*@R{iTt8hdl2DUiZE-8oJkL z|C!(RUmrhkWY?XUFR}Od;n7|uDiWF9(~(C#GcT<_`*8N)F>jMgJL`U%KYZN#?&~9W zeqT6p^sbyr!NBXKW3fK>_FjMSVI}6o?k$%dc7I+yadP*)*I!nD@rVR8#LH0^LN;8v zO+vTjgvwysi5W`x_MBP`!46_yBHG+(ezQm?iC{_U%vHCibdl^_%Q|wsye00FG6E#; z=hp6*?k4pgLN$@bW952C)n6Gfwjxu$=k>^G`W2v83ZPRNWvbv*ZIRjoa(|1)gY=4B zn*9_)=eP4wOb|H$07)MP2`}{R^Q3OaIKa^3O_!NOAnXXEY7h>tLTJ;Wd=0|o?&i&6 zmXER&-yej5j3Pkf*GaWncXPpU7Z_+b4u2N1bRIdaF0p>(iM=Zf=uU=;|AXpoU2*_h z;r#3`s%uUa0_0Vrga5BouQNOObob|DuMXeY-QXU&W0TUEfxXS1`?t@0ZU*wLp6_>V zs@Q+;2*8Ey-1VPSzwB8z)#PDZxl`kBs^9qBawn|ywVtQp=@<8o-g%?{57iIv+FZG& zdiwMR>3^uc{q{0!*)i?aJjM}G`K#)o8@$i`P4(y)Clig1?5QP(uSe>`F8xFG-`;&G zU;ZWcr|P(=z9F90I>P4`Ej_V+s;*eF#i{fU)%*22SYgHn-%Le9xBN@>n`VuFs_viA z(O7bac>huVA(`&n`B=GoxuG0J&nfkcH}{)1<)xMxl*JxIrEu`Qf;$_EEvDM*$eJ7Jxwm z<|M!@-aQCoVNpDQax{^xSl`FilFILJ;j&x#012^JKmx_*0qHot?IkPckwhii?IWEs ztbxZcQP3G;DQqcAmh7^oZ7oL;6W}`nNE3a|-9s?;b2LD~Ub7i;m#Qa$Bo;pp5bS~D zyKg-mxVD#;%mXLkpo>%lhXn&_yj!H2mvnL>nfQ$Lq;P&)^^LFliz_)ThVU7LFoHwZ zcnTmcu%*QF(RTOvY^W-T#s*qcGz?S|nbg4zSyNwyt)BH$OuP$bB(nmF?mV_FkJ*m! z&wH`ar^*vhpdpotH^&Z~ubMkngWn{s0Ty?Y3?!~L9G9M{bMWxQby)}iO!V(LA7=~f z>+m;cfiddz)jGJcyX+hgGA9XxeYo6j=wv=5tc-LAsE4gLw$Opt`38Vx%-X|Xk^qjH z!9$GiyjE!>KeaKSc39YSbTf7W#n1~KtrEG&6-V6~lcAp-5)4l($MV0c?OBM`EUVDN z%mRY>=2#IeMGOqa1-V5jWam|*q6HO?=XN$?kJ>B!~&jSxp>tNfQYs?aJImm zqw;XsqGKYDUOt8l%k}upq|!&mWcMo4kqfg&yBa-pSs+oy&TxB@^*LeSp_sC$tOVmq zD3hruSF=KUg7rsS$;uO479mbJF$rL`us*g=uVk>tX(?6NU{WEM!V!R{QLYaIzgzNr%gtW4Xts^khCIvWLm zhp|NGcay~MYZ5Td&!2@FfndfdlImtA0EYO1XhQ@QcSw$-FNbtm`~BN=7`(@|J>g-1 z_l7)QqkW~ymyh%Xtf+V{#294P96UT~qrP6F2fm+qVZN;8Djf%1b{q^^IEKZ+n6F2;3wH(Uz+wq z-*bow{|Y42I;3r!GWZcUoN4GSu0L*&n6@gyiu_GZY}_H;K$y|3dq^#KNTJdCBbi#Y zP~*1!G7xAeXRRv~Dx*(j=EiLR;8qja?-~r8AhGaN!GYDt^oUY`p-QF1QKexXP`3>D zU7B^64#}GyP|tiqU=OBXd0|R8>JQm%EE~aHT&1nsL)AjG4CCGMH(`3_{7hx9pd0i$ zYAw?m&uo12j1C2;w-h>STXzhM$b&{%M2MCMOZk=h7e-w-1p16o+w(5Nn zE=(z1OtUt>IUx9**w82c!^wMt#eILz!7!RhqTunoUIwVWn{Xm zAkq30!~%{B1* zF?Hj5O_!wOqk=TWX1 z?)65Xd)io*f-o?`IHdrIGnL}yD6>n%D(ZLM{da!uK1BMGoarnOHQTiwP3*Ej52aC7 zTR9xyP61%QAioC)F|?Gz3||!Rndm+A7-~ntMr^IyP)DwoELssETsF0L6A$QjrEmcv zbZ2^f`}LPql>4kz4a`H=M=9j;FS2i69-ru40FLcIjxvRzAODDbSida$U<^gq``)58 zC>EFOl5Q)y$pqk#VFYGkLk~ct=-u}s0KqcS?^Y|CcmQyj42Q0X(t?Sf&{hnVTPwr| zLcIKPkyFbbBEN(k`!b38wmxa)N|s8E)kP$Hc(QK4R9Tx0h(B|Hnl=bRM)fWl2f4z^2% z=ZLj+I%VAUfSXaT@`be5J|N+g%05!{!BdvgqHQLj+*KzS!oQj(alQN)^-@+FMRRLO z+}l_l!1HOl*dNK9@|^x20?RLmWg=UBf(SLcx8reHddPrG5dewUl!)Z~AOeW!j3@SQ zBPodf&Lg1by$GBy7>1_Y}+Us{ATNgRLXczD|3%efWL1QNCG+>F;Ga--u@TAIr2O>sSFomZNWm(0hjz z1dUd9>efP|pwkV(p*Di!5kf-}zULf)htcJGi7mi%I2sk|kLN4O**i!EaHRm*&9s5g zM~ABVr<_Lsv{$C_rJw1yUF;<(%O!fMRi$;0JZdyO*-(Tfa181ps8orUIh3*ZGS>1` z?n{i40R7ZAKpH;b>n6(f3bGpB#eS=V+}k}BJtJA9H!_hzyoW>dDR_BWx| zNz}Hj8&AQ2oZy+)1jrc^3LPkqCiVbm4t44wM(~gQ|e~QwO@I+HkW72Aw1`7=Fda(WiTikqS7Z; zX=TTa;VDtmpYsR$9DVf!c#}XxV458x0E$0AL%D7Kl7uWkhd(oHg&Z8j_``&?WE6aY z&ZVQ^3;l@gek6z!fR#(+EsF5n{%Zwot43X0Rd(^ zPWTDyjf&Q0mGBHgcz5wu88j$#nUU_rZYQ5M#+0>kxKCUGE^Y7C(^bY1LYI$o2#~?WbLxBzFXYIlR|jSvVqZ9(-XT{ zGgI{8J_Jt6HdMeiN)GUmN_f(>i~EwHeUpMAJ8pywJjE=xYxNozsx%M9k&}C1XWXTJ*7UoSZmJF-Vxwxr3+p zadx%>XzTHI0UDHXnw22wciny`A8I>yi=QsIUMH6IGPYbmrmX2W>wpBhOzQyh_@o4y z`6%Ylwb-qrF3^(nObpsI07o30Vww3qeSP2}PN+3FsU4rj2H3U~@125vZ9QVaG~u;> zQtt^1_SyYK6ObxGxqOh!mFCsQ(R(g_xqkfmU73Q8l!Cr6zLz9E`6M5LmCa!o$hr$T z6p{f|e2j7Y-wHd;);=0-vB*kC~^xm~iU2xy+1~limyP3G5HNADov+!#S4-k2b4elmb zL-T~gjR~X9iE9y;QnnK^J^4&2rw~Iws*&q1#!2{^=XcT?;XcP&nM)E5XZvAsDL`kW zl=0CMDwe7rr?V@FACz&7o8)XAO3H#DN5X|yihR>YW4IIPK)B4cpS?_(paO^|l0PcS z>A%k!Bl}9Ev(+6Z7NjAkVD1k`mbGB&0+DytM#DaKWK;}Vz+D0qejmW~oQs-2H<7WA zVM6|_x_z(;{%QFA`*%s->43z(IS*wZ;aUJWk$9&7 z6yPNWf>w@4-GyY5wlq zvdOK*KiSu>D6heRQTB2V*ILv%Qixl6nI}cX`VoEK`|=W?idoqJ3qOE!;Ka$?pOa$= z#6X2YR_6NmW6)(jsRe{Ip(MJco+p$ft<0rP^zAnF8gBd$1aAl$Tpx7ibd;I^PN~Qw zYOb)qCku7{fJ3qU~WO)aB)` zQqubA>J6)7^oYfHE_Ab`HdiP)O1vn_!&IxY!}{lIJ7T@UiP`4 z?Q*kj;wE2|`r1$cs&JgxCzOvv8*R|!KG2-FK7>#hvKU2Ca;!)A)x>_n|F6$N$i~bao^CtjXRN zX8WLRH~9k8Nc|x{{EN-6A8npj^%zk)m@vob0;NNUO;@ERQF1(O2{>O6+zF>Ug!?v# za$iMF?7@VM97{?#P&Nmlz2r=9-o$uWm?(`u8GUiznbPd?e^o*s94##xBWtF)l4%2;1R>1Z;t#@oShf)pkv1!%kbN* zwc_B;v?!y4+ls3|dNeBsYA7`VG-MbZ($IVYNP8L=aX4_!UYjMr zAl3q&JetkIU3IIS0-C!thY&5DOTdcju+SO}`06+qJR5MRcDwBMR;>8Rfg9en8$AcY zq3T)`P!U+`d9AK}jD!X8%cIbcnkY=>87zR};ghDFZp_o}I5a@Q5{uS7c7SFQ=J(`y z1E|r>;vuyx?Z5mvBfb54lV+h^Wga`}xMNv~!@BHCURm2z@^ecGZ=|;w+}n`BLI_-a zHT(hyJfX}^NAnrT!M#z>eqfR_M@f9 zTbHY6C^)zSU9cDVb0=?mdI?TNrLpsuj@&5lD?{SyUUrGVSr;?(&VfeWRY zV@5)Ap*VTOzTR)?a@aZdleEjF5G0qQD1c=S+nj1&xJ))QIKJo9xzYm=yP;DntKKt5t8 zVi-THu{GT*LWr8JLx7mler(o4=rBEUi-+0gk?Ql9HYXt1vFHVG9bXLqf#Y+IBYSmp zSnKW0uci$(CCUTM)b-US+sfIp;re^oAmG}&?^!d~i_jB^+ly$q#RsZv3lAFP&aNC= zhOt-VdOpTn(X5H`;`61>zLg_&?QiE}cFljc2DGg2;ggc@Z5vzNW~X}H2XfHFWgyj& z^xpIBb4*v`!iysktp~huP*0?l$l4Rfcze`H)a{@97NmbE*Af%9NId==dvfpdFR{Op zr@o#zdFk!<6TiAY{Wy8@_4i*}Rv^F@MZ!7aNLDln!FCno#s<5!^>&(;J;b!Q6TLx2xxZ3oE!(eJi4ol|TtJPui* zL|hiWZR0^&0P>P1@YaKn)vY%5F-lJ9zHRKid zXnxT0tCi4%YcaxYBP)HSLp-=pOQHd&x~~}eYx#Z?@;y-$7d~w2hguiU1LC|9qXdf> zTOlIQqm=hS<_Q3$T_9yK##0$^}N$hQJO4Y#b?oz}0-@z*`pg5`2SAm-x8EfpLPmp|K! zdGzuYroqn*@`ETRb4yxht!0_G z?qY$|_;_(Z+^4**)K`&e-UB4tboEnMx-p-`{rjH?>IK$5D>>t|Lu*h@^A26hCLR|| zmdIhYEKhc>nmH>7F15X!_8u;Fehv#kK@_kKMBm>jo=Hu8?2 zjubZIYYP?PQLn~>6B1TW3X*|>X@XRo77%Uoo2Ryy>EakGt_dr4N&L`Q%L#D}XZk%A zhMrWOskji1=xETGaGKnETr+V&L(wheya-G2d}7WPBmmg#;f!_YXZJ;sJEZ#hAN)2cYHX7B)Y%SJ;|(v!ca*NFV9d; zym4Pl;oYT@DHxsf3n}g;5x=L=eqQ*oEiv*dV{jEJ}@G<)?_SV-<&-g~} z&_b7gu~D~7b4+Q-35U7wSf&{P7yK&o&BFzLr?rA8;eY|7>xO{#_FU9t$Pf!_K6`O* zP=Qbtz}^X1yyz5}@hnH=zM_u|YyXmPPStVCstaoZ4PnB=z1v$A)H@V?Gb_do*N&0H z2U<^FU2}?u|9EQgq~sKe?ENgTsfKTd_wSeQ9>WtkqAf_a@-|Fi-*;#zjOcDuKB|{J zRHu}eVwjf(km-gUnE;uEqh#Z`EYK?r6@dn)(^`J=1@0LZUfE__#aS%r~5?!En0)Wf_%1c~LBJ8@vkJkD#K2ihVP$qKjsK zX0HmuY>`%0AR_J=I)26^oMRII#)Jz@uFfMLyc4%!w)4D7fi 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

    )7WAg19*gS= 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%0D+xX&L&HEi(jMvB2 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

    UJzTQbq5%s=ZXfPjKan??kc4 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%0A|c6vNaKA61%%n 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

    x^rmglg2(!;SP%vyeaG)D4hqPIy% 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