接着我们上一讲的结尾,我们获取到了一个高频次排序好的列表,接下来的任务就是要把这么一大堆的单词进行翻译,我们想要得到每个单词的音标,有什么词性以及对应的翻译。现在我们就来讲讲通过网络来实现单词的翻译。
Java的HttpURLConnection
类可以帮助我们发送HTTP请求,并获取相应的HTTP响应。我们可以设置请求头、请求方法、请求参数等信息,来模拟浏览器行为。
java"><dependency><groupId>org.apache.httpcomponents.client5</groupId><artifactId>httpclient5</artifactId><version>5.1</version>
</dependency>
<dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.15.3</version>
</dependency>
private static final String TRANSLATE_URL = "http://www.baidu.com/s?wd=";
这个网页就是我们平时百度查词的地址
我就希望可以得到两种音标,还有对应的词性和翻译。具体用到的两个依赖,一个是对于网页的解析Jsoup,这个和python的BS4基本原理一致,还有一个就是用于网络请求的http依赖。
java">import org.jsoup.Jsoup;
import java.net.URLEncoder;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.net.HttpURLConnection;
import java.net.URL;
第一步我们首先要对查询的文字进行一个编码(这一步在项目里不需要有,因为前序操作已经确定这个是String类型了)
String encodedWord = URLEncoder.encode(word, StandardCharsets.UTF_8.toString());
// 编码查询词,防止特殊字符导致的URL解析错误
第二步是向url发起链接
// 构建完整的URL,包括查询参数
URL url = new URL(TRANSLATE_URL + encodedWord);
// 打开连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
这里我们将URL封装成了带有网络协议等信息的实例对象来方便我们调用Connection
方法。
现在建立好连接之后,我们可以看一下连接里面的数据。
可以看到请求体里面没有任何的数据段,这样去进行多次读取页面的操作会让百度感觉很”陌生“,所以现在要去请求头里加一些数据,让这个访问看起来就是真实的。
java">// 创建一个可变的 Map 来存储请求头Map<String, String> headers = new HashMap<>();headers.put("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");headers.put("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");headers.put("Connection", "keep-alive");headers.put("Cookie", "BIDUPSID=2BCC6FC9896B4237256E7EC335CECF0A; PSTM=1726058803....");headers.put("Host", "www.baidu.com");headers.put("Upgrade-Insecure-Requests", "1");headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0");
这些内容都可以直接在自己的网页端查到。我们用一个map来进行存储,然后一次添加到
// 添加自定义请求头
for (Map.Entry<String, String> entry : headers.entrySet()) {connection.setRequestProperty(entry.getKey(), entry.getValue());
// 设置请求方法为GET
connection.setRequestMethod("GET");
到这一步我们的请求就算是发送完成了。接下来就是服务器的响应,200就是响应成功了
java">// 获取响应码
int responseCode = connection.getResponseCode();
StringBuilder response = new StringBuilder();
// 如果响应码为200,表示请求成功
if (responseCode == HttpURLConnection.HTTP_OK) {// 获取响应流BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));String line;// 读取响应内容while ((line = reader.readLine()) != null) {response.append(line);}reader.close();
}
在 Java 的网络编程中,当建立了一个网络连接(比如通过 Socket
连接或者 HttpURLConnection
等建立的连接,这里的 connection
就是代表这样一个已建立的连接对象),可以通过调用 getInputStream()
方法获取这个连接对应的输入流。这个输入流里包含了对方(服务器等)发送过来的数据,不过它是以字节流的形式存在的,原始状态下不太方便直接按文本内容进行读取操作。
InputStreamReader
是字节流和字符流之间的桥梁,它用于将字节流(也就是前面获取的connection.getInputStream()
这个字节输入流)转换为字符流,因为很多时候我们期望处理的是文本字符形式的数据,而不是单纯的字节数组。同时,指定了编码格式为StandardCharsets.UTF_8
,这一点很关键,因为要确保正确地将字节解码成对应的字符。不同的字符编码格式下,相同的字节序列可能表示完全不同的字符,如果不指定正确的编码(服务器通常会按照某种编码格式来发送文本响应,UTF-8 是现在网络通信中很常用的一种编码格式),就可能出现乱码问题,导致后续无法正确解析和处理读取到的文本内容。
又有一个问题,为什么不直接使用
connection.getInputStream()
创建BufferedReader
?
- 字节流与字符流的差异
connection.getInputStream()
返回的是字节流(InputStream
)。字节流是以字节为单位来处理数据的,它并不知道这些字节如何组合成字符。而在很多网络通信场景中,如读取网页内容、解析 JSON 或 XML 等文本格式的数据时,我们需要以字符为单位来处理数据。- 直接使用字节流来读取文本数据会比较麻烦。例如,一个汉字在 UTF - 8 编码下可能占用 3 个字节,如果只按字节读取,很难正确地将这些字节组合成完整的字符进行处理。
- 编码问题
- 没有经过字符流转换直接使用字节流读取文本可能会导致编码问题。不同的字符编码方式(如 UTF - 8、GBK 等)对字符的字节表示是不同的。
- 通过
InputStreamReader
可以指定编码方式(如StandardCharsets.UTF - 8
),将字节流按照正确的编码转换为字符流。这样能确保从网络中读取的文本数据被正确地解码。例如,如果服务器以 UTF - 8 编码发送数据,而客户端没有正确地按照 UTF - 8 解码,就会出现乱码。- 缓冲和高效读取
BufferedReader
提供了缓冲功能。它内部有一个缓冲区,当读取数据时,会先从缓冲区获取数据,只有当缓冲区为空时才会从底层的输入流(这里是经过InputStreamReader
转换后的字符输入流)读取数据并填充缓冲区。- 这种缓冲机制可以减少与底层数据源(如网络连接)的交互次数,提高读取效率。直接使用
connection.getInputStream()
没有这种缓冲机制,每次读取操作可能都会涉及到相对耗时的底层 I/O 操作。例如,在读取一个较大的文本文件或网络响应中的大量文本内容时,缓冲机制可以显著提高性能。- 方便的文本读取方法
BufferedReader
提供了方便的readLine()
方法,可以逐行读取文本内容。在处理文本数据时,很多时候数据是以行分隔的,如网页的 HTML 代码、配置文件等。- 直接使用
connection.getInputStream()
作为字节流没有这种按行读取的便捷方法,需要自己编写复杂的代码来实现按行读取字节流并将其转换为字符的功能
总之,reader读到的信息就是获取的那个页面的源码。我们把他存到一个String对象里面。
接下来才是真正意义上的爬取操作。
java">Document doc = Jsoup.parse(response.toString());Elements posElements = doc.select("span.part-name_3R3ee");
Elements En_phoneticElements = doc.select("span.orginal-txt_3dDqw");
Elements US_phoneticElements = doc.select("sapn.orginal-txt_3dDqw");
能看到在html标签里面,英式的发音藏在一个span标签里面,相对应的翻译
我就不一一列举了,用doc.select方式找到对应的标签进行提取。我们可以定义一个DTO或者一个数据库按照类型把他们都存进来。
wordModel.setWord(word);
wordModel.setEnPronunciation(En_phoneticElements.first().text());
wordModel.setUsPronunciation(US_phoneticElements.first().text());
根据这种写法就得到了我们的音标。但是又有个问题,音标只有一个,但是单词的翻译可能会有很多个,所以单纯的提取词性和翻译就会有问题。
我们得到两个travel的词性,理论上我们需要通过一个for循环来获取。通过分析源码,我们可以看到代码一个词性和对应的翻译都放在一个大的div标签了。
但现在又有一个问题就是这个翻译并不是一个字符串,是一个列表,多个span标签组成的。所以我们想要把翻译搞到一起就得多写两句代码,把列表换成字符串。
Elements translationElements = parent.select("span.mean-text_4MwRe");
StringBuilder translations = new StringBuilder();for (Element element : translationElements) {// 去除分号,避免重复String text = element.text().replace(";", "").trim();if (!text.isEmpty()) {translations.append(text).append(";");}
}
把数据格式安排明白之后,我们就可以继续存储数据了。
java">for (Element posElement : posElements) {// 获取当前词性的翻译和音标Element parent = posElement.parent();if (parent != null) {Elements translationElements = parent.select("span.mean-text_4MwRe");StringBuilder translations = new StringBuilder();for (Element element : translationElements) {// 去除分号,避免重复String text = element.text().replace(";", "").trim();if (!text.isEmpty()) {translations.append(text).append(";");}}// 创建一个结果映射Map<String, String> result = new HashMap<>();result.put("translation", translations.toString());result.put("pos", posElement.text());results.add(result);}}
最终的results当然是多个map映射的列表了,不要嫌麻烦,我们继续转换数据,把得到的词性翻译列表转换成一个可以直观的字符串。
java">StringBuilder combinedResult = new StringBuilder();
for (Map<String, String> result : results) {String pos = result.get("pos");String translation = result.get("translation");combinedResult.append(pos).append(translation).append(" ");
}
wordModel.setDescription(combinedResult.toString());
这样的话,我们就把所有的一个简易的爬虫式的单词翻译讲清楚了。我们后续会继续讲如何把查询到的翻译存到本地数据库。