【问题标题】:BufferedReader can't read long lineBufferedReader 无法读取长行
【发布时间】:2020-03-01 19:51:53
【问题描述】:

我正在将这个文件:https://www.reddit.com/r/tech/top.json?limit=100 从 HttpUrlConnection 读取到 BufferedReader 中。我已经让它读取了一些文件,但它只读取了它应该读取的大约 1/10。如果我更改输入缓冲区的大小,它不会改变任何东西 - 它只是以较小的块打印相同的东西:

try{
    URL url = new URL(urlString);

    HttpURLConnection connection = (HttpURLConnection) url.openConnection();

    BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));

    StringBuilder sb = new StringBuilder();

    int charsRead;
    char[] inputBuffer = new char[500];
    while(true) {
        charsRead = reader.read(inputBuffer);
        if(charsRead < 0) {
            break;
        }
        if(charsRead > 0) {
            sb.append(String.copyValueOf(inputBuffer, 0, charsRead));
            Log.d(TAG, "Value read " + String.copyValueOf(inputBuffer, 0, charsRead));
        }
    }

    reader.close();

    return sb.toString();
} catch(Exception e){
   e.printStackTrace();
}

我认为问题在于文本都在一行上,因为它的 json 格式不正确,而 BufferedReader 只能占用一行这么长的时间。有没有办法解决这个问题?

【问题讨论】:

  • 无效 - 文件的 1/10 左右
  • 注意:StringBuilder 有一个 append(char[], int, int),这应该比创建一个 String 更好
  • 您说的是阅读一行,但我没有看到与的阅读/处理相关的代码。我所看到的只是读取 blocks 个字符的代码。如果你想要使用readLine() 方法。除了可用内存和 Java String 的最大长度之外,它对行的长度没有限制。
  • 最初我使用 readLine() 这个方法: String line; while ((line = reader.readLine()) != null) { sb.append(line + "\n");由于整个文本文件是一行,因此使用 while 循环读取一行似乎很愚蠢
  • 有没有可能是这部分链接的问题:...?limit=100?

标签: java json parsing reader


【解决方案1】:

我认为问题在于文本都在一行上,因为它的 json 格式不正确,BufferedReader 只能占用这么长的一行。

这个解释不正确:

  1. 您不是一次阅读一行,BufferedReader 没有将文本视为基于行。

  2. 即使您一次从BufferedReader 读取一行(即使用readLine()),对行长度的唯一限制是Java String 长度的固有限制(2^31 - 1 个字符),以及堆的大小。

另外,请注意“正确”的 JSON 格式是主观的。 JSON 规范没有说明格式化。 JSON 发射器通常不会在格式化人类很少阅读的 JSON 时浪费 CPU 周期和网络带宽。使用 JSON 的应用程序代码需要能够处理这个问题。


那么到底发生了什么?

不清楚,但有一些可能性:

  1. StringBuilder 具有 2^31 - 1 个字符的固有限制。但是,对于(至少)某些实现,如果您尝试将StringBuilder 增长到超出该限制,它将抛出OutOfMemoryError。 (此行为似乎没有记录在案,但通过阅读 Java 8 中的源代码可以清楚地看到。)

  2. 也许您读取数据的速度太慢(例如,因为您的网络连接太慢)并且服务器正在超时连接。

  3. 也许服务器对它愿意在响应中发送的数据量有限制。

由于您没有提到任何例外,而且您似乎总是获得相同数量的数据,我怀疑第三种解释是正确的。

【讨论】:

    【解决方案2】:

    read() 应该继续阅读 charsRead &gt; 0。每次调用读取时,读取器都会标记上次读取的位置,并且下一次调用从该位置开始并继续,直到没有更多要读取的内容为止。它可以读取的大小没有限制。唯一的限制是数组的大小,但文件的整体大小没有。

    您可以尝试以下方法:

    try(InputStream is = connection.getInputStream(); 
       ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
    
      int read = 0;
      byte[] buffer = new byte[4096];
    
      while((read = is.read(buffer)) > 0) {
        baos.write(buffer, 0, read);
      }
    
      return new String(baos.toByteArray(), StandardCharsets.UTF_8);
    } catch (Exception ex){}
    

    上述方法是纯粹使用流中的字节并将其读入输出流,然后从中创建字符串。

    【讨论】:

    • 我仍然看不出这(虽然公认更短更简洁)与 OP 所做的有什么不同。他也在循环读取流。
    • Janez Kuhar 是正确的,代码运行良好,但读取的数量仍与之前相同
    • "read() 是一个流" 真的吗? InputStream 是一个流。 Reader 是一个流。 read() 是一个可以调用来流中读取的方法,但它不是流。
    • @Andreas 修复了它。 @Janez Kuhar。这只是做同样事情的另一种方式,但作用于原始字节而不是缓冲读取器。可能有一个 BufferedReader 不喜欢的 char 值(尽管非常怀疑),但读取原始字节可能会更好地处理它。
    【解决方案3】:

    我建议使用 3d 方 Http 客户端。它可以将您的代码从字面上减少到几行,您不必担心所有这些小细节。底线是 - 有人已经编写了您尝试编写的代码。它有效并且已经过很好的测试。几点建议:

    1. Apache Http Client - 众所周知且流行的 Http 客户端,但对于像您这样的简单案例来说可能有点笨重和复杂。
    2. Ok Http Client - 另一个知名的 Http 客户端
    3. 最后,我最喜欢的(因为它是我写的)MgntUtils 开源库,它有 Http 客户端。可以在here 找到 Maven 工件,在 GitHub 上包含库本身作为 jar 文件、源代码和 Javadoc 可以在 here 找到,JavaDoc 是 here

    使用 MgntUtils 库的代码只是为了演示您想要在此处执行的操作的简单性。 (我测试了代码,它就像一个魅力)

    private static void testHttpClient() {
        HttpClient client = new HttpClient();
        client.setContentType("application/json; charset=utf-8");
        client.setConnectionUrl("https://www.reddit.com/r/tech/top.json?limit=100");
        String content = null;
        try {
            content = client.sendHttpRequest(HttpMethod.GET);
        } catch (IOException e) {
            content = client.getLastResponseMessage() + TextUtils.getStacktrace(e, false);
        }
        System.out.println(content);
    }
    

    【讨论】:

      【解决方案4】:

      我的猜测是您的默认平台字符集是 UTF-8,并且出现了编码问题。对于远程内容,应指定编码,而不是假定等于您机器上的默认编码。

      响应数据的字符集必须正确。为此,必须检查标题。默认值应该是 Latin-1,ISO-8859-1,但浏览器会解释它 作为 Windows Latin-1,Cp-1252。

              String charset = connection.getContentType().replace("^.*(charset=|$)", "");
              if (charset.isEmpty()) {
                  charset = "Windows-1252"; // Windows Latin-1
              }
      

      然后您可以更好地读取字节,因为读取的字节数和读取的字符数没有确切的对应关系。如果在缓冲区的末尾是一个代理对的第一个字符,两个UTF-16字符组成一个Unicode字形,符号,U+FFFF以上的代码点,我不知道效率底层的“修复”。

              BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
              ByteArrayOutputStream out = new ByteArrayOutputStream();
              byte[] buffer = new byte[512];
              while (true) {
                  int bytesRead = in.read(buffer);
                  if (bytesRead < 0) {
                      break;
                  }
                  if (bytesRead > 0) {
                      out.write(buffer, 0, bytesRead);
                  }
              }
              return out.toString(charset);
      

      确实这样做是安全的:

      sb.append(inputBuffer, 0, charsRead);
      

      (复制可能是一种修复尝试。)

      顺便说一句,char[500] 占用的内存几乎是byte[512] 的两倍。


      我看到该站点在我的浏览器中使用 gzip 压缩。这对于诸如 json 之类的文本是有意义的。我通过设置请求标头 Accept-Encoding: gzip 来模仿它。

          URL url = new URL("https://www.reddit.com/r/tech/top.json?limit=100");
          HttpURLConnection connection = (HttpURLConnection) url.openConnection();
          connection.setRequestProperty("Accept-Encoding", "gzip");
          try (InputStream rawIn = connection.getInputStream()) {
              String charset = connection.getContentType().replaceFirst("^.*?(charset=|$)", "");
              if (charset.isEmpty()) {
                  charset = "Windows-1252"; // Windows Latin-1
              }
              boolean gzipped = "gzip".equals(connection.getContentEncoding());
              System.out.println("gzip=" + gzipped);
      
              try (InputStream in = gzipped ? new GZIPInputStream(rawIn)
                      : new BufferedInputStream(rawIn)) {
                  ByteArrayOutputStream out = new ByteArrayOutputStream();
                  byte[] buffer = new byte[512];
                  while (true) {
                      int bytesRead = in.read(buffer);
                      if (bytesRead < 0) {
                          break;
                      }
                      if (bytesRead > 0) {
                          out.write(buffer, 0, bytesRead);
                      }
                  }
                  return out.toString(charset);
              }
          }
      

      可能是因为不符合 gzip 的“浏览器”压缩内容的内容长度在响应中设置错误。这是一个错误。

      【讨论】:

      • 已经实现它似乎没有做任何事情。仍然只有 10 个 reddit 条目而不是 100 个
      • 我有一个错误,使用 getContentEncoding ("gzip") i.o.获取内容类型。我会在我的答案中添加一些东西
      • 这么多数据丢失似乎表明 reddit 中的一个错误,响应设置了压缩数据的 Content-Length,尽管提供了 gzip 压缩数据。可以测试一下。
      猜你喜欢
      • 2013-10-30
      • 2015-10-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-09
      • 2013-09-02
      • 1970-01-01
      • 1970-01-01
      • 2015-05-12
      相关资源
      最近更新 更多