NLTK e utilizadores

Depois de instalar o NLTK e o Wordnet, tudo correu bem até ao momento em que quis usar o lematizador do wordnet com outro utilizador, que não o que o instalou.

Instalei o NLTK como root e depois quis corrê-lo como um utilizador normal; e foi aí que surgiram os problemas: o Python queixava-se de que não encontrava o wordnet.

Verifiquei as pastas e as permissões, e parecia tudo bem. Cheguei a passar a pasta nltk_data para o modo 777, e com pertença ao utilizador e grupo nobody.

No script, fiz:

nltk.data.path.append('/root/nltk_data')

e no erro, o Python dizia que tinha consultado essa diretoria:

  Searched in:
    - '/root/nltk_data'
    - '/home/banha/nltk_data'
    - '/usr/share/nltk_data'
    - '/usr/local/share/nltk_data'
    - '/usr/lib/nltk_data'
    - '/usr/local/lib/nltk_data'
    - '/usr/nltk_data'
    - '/usr/share/nltk_data'
    - '/usr/lib/nltk_data'

Mas o problema era mesmo o facto da diretoria nltk_data estar pendurada na diretoria root. Copiei essa diretoria para a diretoria mais acessível de todas – tmp – e passou tudo a funcionar.

Não esquecer de colocar no script:

nltk.data.path.append('/tmp/nltk_data')

Python e JSON

Eis um primeiro exemplo de criação de uma string JSON, que contém duas listas, em Python:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import json

lista1 = [3,4,-2,0,17]
lista2 = ["Abel", "Maria", "Carolina", "Zeca"]

import json
data = {}
data['num'] = lista1
data['name'] = lista2
s = json.dumps(data)
print s

cujo output é:

{"num": [3, 4, -2, 0, 17], "name": ["Abel", "Maria", "Carolina", "Zeca"]}

Stopwords em português e pontuação

Existe um pacote em Python para remover as stopwords de textos em português. Eis como se usa:

# Stopwords em português:

>>> import nltk
>>> nltk.download('stopwords')

>>> stopwords = nltk.corpus.stopwords.words('portuguese')

>>> stopwords[:10]
[u'de', u'a', u'o', u'que', u'e', u'do', u'da', u'em', u'um', u'para']

>>> len(stopwords)
203

>>> 'a' in stopwords
True
>>> 'xico' in stopwords
False

Neste momento tem apenas 203 palavras, o que me parece muito pouco. Há uma lista destas palavras no github.

Para testar a pontuação, pode ser usado o código seguinte:

>>> import string
>>> for c in string.punctuation:
...     print("[" + c + "]")
...
[!]
["]
[#]
[$]
[%]
[&]
[']
[(]
[)]
[*]
[+]
[,]
[-]
[.]
[/]
[:]
[;]
[<]
[=]
[>]
[?]
[@]
[[]
[\]
[]]
[^]
[_]
[`]
[{]
[|]
[}]
[~]

>>> ',' in string.punctuation
True
>>> 'a' in string.punctuation
False

Argumentos para o script

O Python tem várias formas de gerir argumentos passados a um script. No stackoverflow indicam vários, com exemplos de utilização.

Os exemplos seguintes usam o argparse, uma biblioteca standard, e parte deles foram retirados do Argparse Tutorial.

import argparse
parser = argparse.ArgumentParser()
parser.parse_args()

No exemplo anterior, utiliza-se a configuração mínima. A classe ArgumentParser cria um parser mínimo com as funcionalidades apresentadas nas utilizações seguintes:

$ prog.py
$ prog.py --help
usage: prog.py [-h]

optional arguments:
  -h, --help  show this help message and exit
$ prog.py --verbose
usage: prog.py [-h]
prog.py: error: unrecognized arguments: --verbose
$ prog.py foo
usage: prog.py [-h]
prog.py: error: unrecognized arguments: foo

No exemplo seguinte, adiciona-se um argumento posicional, com informação para a opção de help.

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("echo", help="echo the string you use here")
args = parser.parse_args()
print(args.echo)

A função add_argument adiciona um argumento, e o parâmetro help permite adicionar informação de ajuda. Eis os resultados de utilização.

$ prog.py
usage: prog.py [-h] echo
prog.py: error: too few arguments

$ prog.py -h
usage: prog.py [-h] echo

positional arguments:
  echo        echo the string you use here

optional arguments:
  -h, --help  show this help message and exit

$ prog.py ola
ola

De seguida, apresenta-se um exemplo com um argumento numérico inteiro.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("square", help="display a square of a given number", type=int)
args = parser.parse_args()
print(args.square**2)

E algumas utilizações desse código:

$ prog.py 5
25

$ prog.py r
usage: prog.py [-h] square
prog.py: error: argument square: invalid int value: 'r'

Um exemplo de uso de parâmetros opcionais:

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--verbosity", help="increase output verbosity", action="store_true")
args = parser.parse_args()
if args.verbosity:
        print("verbosity turned on")

Este programa não mostra nada se não houver parâmetros. No entanto, ao passar-se o parâmetro –verbosity, imprime a frase “verbosity turned on”. O parâmetro action=”store_true” indica que se o argumento –verbosity for passado, o valor True é atribuído a args.verbose. Caso contrário é-lhe atribuído False. Ver usos abaixo:

$ prog.py

$ prog.py --verbosity
usage: prog.py [-h] [--verbosity VERBOSITY]
prog.py: error: argument --verbosity: expected one argument

$ prog.py --verbosity 1
verbosity turned on

$ prog.py -h
usage: prog.py [-h] [--verbosity VERBOSITY]

optional arguments:
  -h, --help            show this help message and exit
  --verbosity VERBOSITY
                        increase output verbosity

Por fim, um exemplo com parâmetros opcionais e valores por omissão.

import argparse

parser = argparse.ArgumentParser()
parser.add_argument("palavra", help="palavra a pesquisar nos discursos")
parser.add_argument("-c", "--componentes", type=int, default=10, help="numero de componentes")
parser.add_argument("-w", "--words", type=int, default=20, help="numero de top words a mostrar")
args = parser.parse_args()

palavra = args.palavra
n_components = args.componentes
n_top_words = args.words

print "palavra = " + palavra + ", n_components = " + str(n_components) + ", n_top_words = " + str(n_top_words)

E alguns usos:

$ prog.py xpto
palavra = xpto, n_components = 10, n_top_words = 20

$ prog.py xpto -c 12
palavra = xpto, n_components = 12, n_top_words = 20

$ prog.py xpto -c 12 -w 25
palavra = xpto, n_components = 12, n_top_words = 25

scikit-learn

Estou a usar um pacote novo, o scikit-learn, para extração de tópicos com os algoritmos Non-negative Matrix Factorization e Latent Dirichlet Allocation.

Para instalar o pacote usei o comando usual:

pip install 'scikit-learn'

Foi preciso instalar também o scipy:

pip install 'scipy'

O pip pediu para ser atualizado e usei o comando sugerido:

pip install --upgrade pip

MongoDB

Vou usar o SGBD da moda, o MongoDB, nos projetos em que tenho estado a trabalhar com o Python.

O Slackware não traz um pacote de instalação do MongoDB, por isso tive que o criar a partir do código fonte. É claro que não o criei de raiz, usei como guia/base os scripts do SlackBuilds.

O script para criar o pacote do MongoDB precisa de um outro pacote que o Slackware também não tem, o SCons, que é um conjunto de ferramentas com funcionalidades semelhantes ao MAKE.

Slackbuilds MongoDB
Slackbuilds SCons

Editei os ficheiros do Slackbuild para colocar os números das versões mais recentes do SCons e do MongoDB.

O pacote do SCons foi criado e instalado sem problemas.

Antes de criar o pacote do MongoDB, tive que consultar a página Build Mongodb From Source que diz que é necessário correr a linha seguinte, a partir do pacote descomprimido do MongoDB:

pip install -r buildscripts/requirements.txt

Sem a atualização anterior, o script do Slackbuild produz um erro. Durante a compilação, recebi o aviso seguinte:

Run ‘pip2 install --user regex‘ to speed up error code checking

Assim fiz. Corri pip2 install --user regex.

O pacote de Mongo DB demorou cerca de duas horas a compilar num i5 com 8GB de RAM, sem processos pesados a correr, e produziu um pacote com 1GB de tamanho (1.089.913.083 bytes).

Quando lancei o mongo, através do script do Slackware, obtive o erro seguinte:

Error parsing command line: unrecognised option '--nohttpinterface'

No manual do mongo, a partir da versão 3.6, essa opção obsoleta foi removida. Assim, removi-a também do script do Slackware, /etc/rc.d/rc.mongodb e já pude correr o script de lançamento /etc/rc.d/rc.mongodb start.

Um exemplo simples de utilização:

> db.users.insertOne({name:'maria',age:32})
{
        "acknowledged" : true,
        "insertedId" : ObjectId("5ab1ce17d199c47e39de4a59")
}
> db.users.find()
{ "_id" : ObjectId("5ab1ce17d199c47e39de4a59"), "name" : "maria", "age" : 32 }
> db.users.updateOne({name: 'maria'}, {$set: {age: 45}})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
> db.users.find()
{ "_id" : ObjectId("5ab1ce17d199c47e39de4a59"), "name" : "maria", "age" : 45 }
>
> db.users.deleteOne({name: 'maria'})
{ "acknowledged" : true, "deletedCount" : 1 }
> db.users.find()
>

Mais informação sobre operações de manipulação de dados: https://docs.mongodb.com/manual/crud/
Manual em geral: https://docs.mongodb.com/manual/

NLTK – Natural Language Toolkit

Vou trabalhar com o Natural Language Toolkit para lematizar palavras e termos em inglês. O pacote NLTK tem que ser instalado. Para isso uso o comando:

pip install -U nltk

Instalei também o Numpy:

pip install -U numpy

Para testar a instalação, entrei no python e digitei import nltk. Depois é necessário importar os dados. O NLTK tem vários corpus de dados. Podemos instalá-los todos, ou selecionar apenas aqueles de que necessitamos. Um dos pacotes é o RSLP (Removedor de Sufixos da Lingua Portuguesa). Eu vou o usar o pacote WordNet. Para isso, entro no Python e digito:

import nltk
nltk.download()

Depois primo a opção d (Download) e escrevo wordnet. Para instalar todos os pacotes, pode escrever-se all. E está concluída a instalação.

Exemplo de uso do lemmatizer:

>>> from nltk.stem.wordnet import WordNetLemmatizer
>>> lmtzr = WordNetLemmatizer()
>>> lmtzr.lemmatize('cars')
u'car'
>>> lmtzr.lemmatize('feet')
u'foot'
>>> lmtzr.lemmatize('fantasized','v')
u'fantasize'
>>> lmtzr.lemmatize('people')
'people'
>>>

Usei como guia, o stackoverflow, How do I do word Stemming or Lemmatization?

Instalar packages

Precisei de instalar um pacote de Python para remover as contrações em inglês. Por exemplo, precisava de expandir “you’re” para “you are”. O pacote que utilizei foi o contractions que está no sítio do Python.

Não domino o mundo do Python, mas aparentemente é necessário instalar o pacote. Provavelmente, poderia ter usado o código fonte diretamente, mas preferi fazer isto da forma sugerida pelo sítio.

Segundo a documentação, preciso de uma ferramenta que o meu Linux não tinha: o PIP. Tive, então que instalar o PIP no Slackware. Para isso, usei um script de instalação do Slackbuilds.

Criei o ficheiro .txz para instalar o PIP com o installpkg do Slackware. Após instalar o PIP é necessário atualizá-lo. Para isso, usei o comando:

pip install -U pip setuptools

Depois, instalei o pacote ‘contractions’:

pip install 'contractions'

Também é possível mandar instalar uma versão específica:

pip install 'contractions==0.0.13'

python-weka-wrapper

Tenho que analisar uns textos em inglês, usando o Python. O primeiro passo é remover as “stop words”, tal como indicado no artigo de Anna Huang, “Similarity Measures for Text Document Clustering”. A Anna usou a ferramenta Weka machine learning, escrita em Java, para fazer isso.

Descarreguei o Weka, e instalei-o no Windows para perceber o que era e como funcionava. Comecei por criar um pequeno programa, em Java, que verifica se uma palavra é uma “stop word”. Além disso cria o ficheiro STOP_WORDS_FILE.txt com a lista das stop words atuais. Adicionei a biblioteca “weka.jar” ao projeto e escrevi o código seguinte:

package stopword;

import java.util.Scanner;
import weka.core.Stopwords;

public class StopWord extends Stopwords {

    public StopWord() {
        try {
            write("STOP_WORDS_FILE.txt");
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] a) {
        StopWord stp = new StopWord();
        Scanner teclado = new Scanner(System.in);
        System.out.println("Insert a word to check if it is a stop word, or press ENTER to finish.");
        do {
            System.out.print("Word or ENTER: ");
            String s = teclado.nextLine();
            if(s.isEmpty()) break;
            System.out.println(isStopword(s) ? "Yes" : "No");
        } while(true);
    }
    
}

Eis o resultado de uma sessão em consola:

run:
Insert a word to check if it is a stop word, or press ENTER to finish.
Word or ENTER: walk
No
Word or ENTER: well
Yes
Word or ENTER: to
Yes
Word or ENTER: never
Yes
Word or ENTER: alice
No
Word or ENTER: 
BUILD SUCCESSFUL (total time: 33 seconds)

Depois, foi necessário instalar o python-weka-wrapper para poder invocar o Weka a partir de um programa em Python.

O python-weka-wrapper precisa do Python 2.7 (não funciona com o Python 3, por enquanto), do JDK1.7+, e do javabridge (versão >=1.0.11), que por sua vez precisa do numpy.

Não sabia se tinha algum desses módulos já instalados e tive que ir ao stackoverflow ver como consultar os módulos instalados. Eis como:

Na consola do Python, fazer
>>>help() 
e depois
help> modules

Descarreguei o numpy e compilei-o. Na documentação, aconselhavam a instalar uma série de outros pacotes, não obrigatórios, mas altamente recomendáveis. Com o risco de não sair deste ciclo de dependências, não instalei mais nada.

Os comandos que usei, para que fique registado, foram:

python setup.py build
python setup.py install --prefix=/usr/local

Obtive o erro seguinte:

TEST FAILED: /usr/local/lib64/python2.7/site-packages does NOT support .pth files

Tive que configurar a variável de ambiente PYTHONPATH, tal como indicado no stackoverflow. Depois já foi possível instalar o numpy.

PYTHONPATH="${PYTHONPATH}:/usr/local/lib64/python2.7/site-packages/"
export PYTHONPATH

Instalei, então, o javabridge 1.0.14, de acordo com a documentação de uma versão ligeiramente inferior (1.0.12).

Provavelmente, estas instruções de instalação são comuns na instalação de pacotes de Python, mas para mim são novidade, ainda.

# Make sure numpy is installed !!!
python setup.py install

Por fim, instalei o python-weka-wrapper que, de acordo com a documentação, se instala da mesma forma que os outros pacotes. Já começa a ser um padrão.

python setup.py install


Com o software todo instalado, pude correr um programa de teste para verificar se conseguia aceder à classe Stopwords do Weka. Eis o teste

>>> import weka.core.jvm as jvm
>>> jvm.start()
>>> from weka.core.stopwords import Stopwords
>>> stp = Stopwords(classname="weka.core.stopwords.Rainbow")
>>> print stp.is_stopword("the")
True
>>> print stp.is_stopword("Alice")
False
>>> jvm.stop()

Ah! E não é preciso instalar o Weka, pois o python-weka-wrapper traz uma versão embebida do weka.jar (versão 3.9.1), na pasta “./python/weka/lib”.

Descobri, entretanto um site com código que usa a mesma configuração que eu. Vou ler com atenção para ver o que posso aproveitar daqui.

Expressões regulares IV

A expressão regular mais trabalhosa, até agora, foi a seguinte, que serve para capturar menções, ou seja, nomes precedidos por @, mas que não são emails.

O mais difícil foi perceber que a expressão (?<=\@)\w+ não devolve o @ para o negative lookbehind anterior, e por isso é que tive que usar o @ também no negative lookbehind (?<!\w\@).

Eis o código:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import re
import collections

line = "@mary call @john or send him an email @ john2@gmail.com also tell @mary that I cannot go. Regards @estêvão"

p = re.compile(ur'(?i)(?<!\w\@)((?<=\@)\w+)',re.U)
r = p.findall(line)
cnt = collections.Counter(r)

print cnt

for key, value in cnt.iteritems():
    print key, value

e o resultado, com contagem de ocorrências:

Counter({u'mary': 2, u'est\xeav\xe3o': 1, u'john': 1})
estêvão 1
john 1
mary 2
gmail 1