web-dev-qa-db-fra.com

Lire des lignes de caractères et obtenir la position du fichier

Je lis des lignes séquentielles de caractères à partir d'un fichier texte. Le codage des caractères du fichier peut ne pas être à un octet.

À certains moments, j'aimerais connaître la position du fichier à laquelle la ligne suivante commence, afin de pouvoir rouvrir le fichier ultérieurement et revenir à cette position rapidement .

Des questions

Existe-t-il un moyen simple de faire les deux, en utilisant de préférence les bibliothèques Java standard?

Si ce n’est pas le cas, quelle est la solution de rechange raisonnable?

Attributs d'une solution idéale

Une solution idéale gérerait plusieurs codages de caractères. Cela inclut UTF-8, dans lequel différents caractères peuvent être représentés par différents nombres d'octets. Une solution idéale reposerait principalement sur une bibliothèque de confiance, bien prise en charge. Le plus idéal serait la bibliothèque Java standard. Deuxième mieux serait une bibliothèque Apache ou Google. La solution doit être évolutive. Lire tout le fichier en mémoire n'est pas une solution. Le retour à une position ne nécessite pas de lire tous les caractères précédents en temps linéaire.

Détails

BufferedReader.readLine() est attrayant pour la première exigence. Mais la mise en mémoire tampon empêche clairement d'obtenir une position de fichier significative.

De manière moins évidente, InputStreamReader peut également lire à l’avance, ce qui nuit à la position du fichier. Dans la documentation InputStreamReader :

Pour permettre la conversion efficace d'octets en caractères, il est possible que le flux sous-jacent soit lu avant le nombre nécessaire pour satisfaire l'opération de lecture en cours.

La méthode RandomAccessFile.readLine()lit un seul octet par caractère .

Chaque octet est converti en un caractère en prenant la valeur de l'octet pour les huit bits inférieurs du caractère et en mettant à zéro les huit bits supérieurs du caractère. Cette méthode ne prend donc pas en charge l'intégralité du jeu de caractères Unicode.

19
Andy Thomas

Si vous construisez une BufferedReader à partir d'une FileReader et gardez une instance de la FileReader accessible à votre code, vous devriez pouvoir obtenir la position de la ligne suivante en appelant:

fileReader.getChannel().position();

après un appel à bufferedReader.readLine().

La BufferedReader peut être construite avec un tampon d’entrée de taille 1 si vous êtes prêt à échanger des gains de performance contre une précision de positionnement.

Solution alternative Qu'est-ce qui ne va pas avec le suivi des octets vous-même:

long startingPoint = 0; // or starting position if this file has been previously processed

while (readingLines) {
    String line = bufferedReader.readLine();
    startingPoint += line.getBytes().length;
}

cela vous donnerait le nombre d'octets correspondant exactement à ce que vous avez déjà traité, indépendamment du marquage sous-jacent ou de la mise en mémoire tampon. Vous devez tenir compte des fins de ligne dans votre décompte, car elles sont supprimées.

6
Jeff

L’affaire semble être résolue par VTD-XML, une bibliothèque capable d’analyser rapidement de gros fichiers XML:

La dernière implémentation Java VTD-XML ximpleware, actuellement 2.13 http://sourceforge.net/projects/vtd-xml/files/vtd-xml/ fournit du code maintenant un décalage d'octet après chaque appel à getChar () méthode de ses implémentations IReader.

Des implémentations IReader pour différents encodages de caractères sont disponibles dans VTDGen.Java et VTDGenHuge.Java

Des implémentations IReader sont fournies pour les codages suivants

ASCII; ISO_8859_1 ISO_8859_10 ISO_8859_11 ISO_8859_12 ISO_8859_13 ISO_8859_14 ISO_8859_15 ISO_8859_16 ISO_8859_2 ISO_8859_3 ISO_8859_4 ISO_8859_5 ISO_8859_6 [.________. ____.] UTF_16BE UTF_16LE UTF8;
WIN_1250 WIN_1251 WIN_1252 [.________]. ____.] WIN_1258

2
user1767316

Cette solution de contournement partielle ne concerne que les fichiers codés avec ASCII ou UTF-8 7 bits. Une réponse avec une solution générale est toujours souhaitable (tout comme la critique de cette solution de contournement).

En UTF-8:

  • Tous les caractères mono-octet peuvent être distingués de tous les octets en caractères multi-octets. Tous les octets d'un caractère multi-octets ont un «1» dans la position d'ordre supérieur. En particulier, les octets représentant LF et CR ne peuvent pas faire partie d'un caractère multi-octets. 
  • Tous les caractères à octet unique sont en ASCII 7 bits. Nous pouvons donc décoder un fichier ne contenant que des caractères ASCII de 7 bits avec un décodeur UTF-8.

Pris ensemble, ces deux points signifient que nous pouvons lire une ligne avec quelque chose qui lit des octets, plutôt que des caractères, puis décode la ligne.

Pour éviter les problèmes de mise en mémoire tampon, nous pouvons utiliser RandomAccessFile. Cette classe fournit des méthodes pour lire une ligne et obtenir/définir la position du fichier.

Voici un schéma de code permettant de lire la ligne suivante sous la forme UTF-8 à l'aide de RandomAccessFile.

protected static String 
readNextLineAsUTF8( RandomAccessFile in ) throws IOException {
    String rv = null;
    String lineBytes = in.readLine();
    if ( null != lineBytes ) {
        rv = new String( lineBytes.getBytes(),
            StandardCharsets.UTF_8 );
    }
    return rv;
 } 

Ensuite, la position du fichier peut être obtenue à partir de RandomAccessFile immédiatement avant d'appeler cette méthode. Étant donné un RandomAccessFile référencé par in:

    long startPos = in.getFilePointer();
    String line = readNextLineAsUTF8( in );
2
Andy Thomas

Je suggérerais Java.io.LineNumberReader. Vous pouvez définir et obtenir le numéro de ligne et continuer ainsi à un certain index de ligne.

Puisqu'il s'agit d'une BufferedReader, il est également capable de gérer UTF-8.

1
CoronA

Solution A

  1. Utilisez RandomAccessFile.readChar () ou RandomAccessFile.readByte () dans une boucle.
  2. Vérifiez vos caractères EOL, puis traitez cette ligne.

Le problème avec toute autre chose est que vous devez absolument vous assurer de ne jamais lire au-delà du caractère EOL. 

readChar () renvoie un char pas un octet. Donc, vous n'avez pas à vous soucier de la largeur des caractères.

Lit un caractère de ce fichier. Cette méthode lit deux octets dans le fichier, en commençant par le pointeur du fichier actuel. 

[...]

Cette méthode est bloquée jusqu'à ce que les deux octets soient lus, que la fin du flux soit détectée ou qu'une exception soit levée.

En utilisant un RandomAccessFile et non un Reader, vous abandonnez la capacité de Java à décoder le jeu de caractères du fichier pour vous. Un BufferedReader le ferait automatiquement.

Il y a plusieurs façons de surmonter cela. L'une consiste à détecter vous-même le codage, puis à utiliser la méthode read * () appropriée. L'autre façon serait d'utiliser un flux BoundedInput.

Il y en a un dans cette question Java: lecture de chaînes à partir d'un fichier à accès aléatoire avec entrée en mémoire tampon

Par exemple. https://stackoverflow.com/a/4305478/16549

1
kervin

Au début, j’ai trouvé l’approche suggérée par Andy Thomas ( https://stackoverflow.com/a/30850145/556460 ) la plus appropriée.

Mais malheureusement, je n'ai pas réussi à convertir le tableau d'octets (tiré de RandomAccessFile.readLine) en chaîne correcte dans les cas où la ligne du fichier contient des caractères non latins.

J'ai donc retravaillé l'approche en écrivant une fonction similaire à RandomAccessFile.readLine elle-même qui collecte les données d'une ligne non pas dans une chaîne, mais directement dans un tableau d'octets, puis j'ai construit la chaîne souhaitée à partir du tableau d'octets. code complètement satisfait mes besoins (en Kotlin).

Après avoir appelé la fonction, file.channel.position() renverra la position exacte de la ligne suivante (le cas échéant):

fun RandomAccessFile.readEncodedLine(charset: Charset = Charsets.UTF_8): String? {
    val lineBytes = ByteArrayOutputStream()
    var c = -1
    var eol = false

    while (!eol) {
        c = read()
        when (c) {
            -1, 10 -> eol = true // \n
            13     -> { // \r
                eol = true
                val cur = filePointer
                if (read() != '\n'.toInt()) {
                    seek(cur)
                }
            }
            else   -> lineBytes.write(c)
        }
    }

    return if (c == -1 && lineBytes.size() == 0)
        null
    else
        Java.lang.String(lineBytes.toByteArray(), charset) as String
}
1
plinyar

RandomAccessFile a une fonction: Seek (pos long) Définit le décalage du pointeur de fichier, mesuré à partir du début de ce fichier, auquel la prochaine lecture ou écriture se produit.

1
east.charm