添加验证码识别单元测试:GIF转PNG、Base64编码、OCR调用逻辑

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Niko 2026-05-05 00:07:26 +08:00
parent 437dbe78db
commit 6abec1860b
20 changed files with 271 additions and 18 deletions

1
.cp Normal file
View File

@ -0,0 +1 @@
/Users/niko/.m2/repository/com/microsoft/playwright/playwright/1.55.0/playwright-1.55.0.jar:/Users/niko/.m2/repository/com/google/code/gson/gson/2.12.1/gson-2.12.1.jar:/Users/niko/.m2/repository/com/google/errorprone/error_prone_annotations/2.36.0/error_prone_annotations-2.36.0.jar:/Users/niko/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/niko/.m2/repository/com/microsoft/playwright/driver/1.55.0/driver-1.55.0.jar:/Users/niko/.m2/repository/com/microsoft/playwright/driver-bundle/1.55.0/driver-bundle-1.55.0.jar

10
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

13
.idea/compiler.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="ets-playwright" />
</profile>
</annotationProcessing>
</component>
</project>

7
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml generated Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

13
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="jbr-25" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

11
pom.xml
View File

@ -22,6 +22,12 @@
<artifactId>playwright</artifactId>
<version>${playwright.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.12.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@ -42,6 +48,11 @@
<mainClass>com.ets.scraper.EtsScraper</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
screenshots/captcha.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -10,11 +10,15 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import javax.imageio.ImageIO;
import static java.nio.file.Files.createDirectories;
@ -345,18 +349,33 @@ public class EtsScraper {
public static String recognizeCaptcha(Path imagePath) throws Exception {
byte[] imageBytes = Files.readAllBytes(imagePath);
String base64 = Base64.getEncoder().encodeToString(imageBytes);
// Convert GIF to PNG (Ollama doesn't support GIF)
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
BufferedImage gifImage = ImageIO.read(bais);
if (gifImage == null) {
// Fallback: send raw bytes if conversion fails
String base64 = Base64.getEncoder().encodeToString(imageBytes);
return callOllama(base64);
}
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
ImageIO.write(gifImage, "png", pngOut);
byte[] pngBytes = pngOut.toByteArray();
String base64 = Base64.getEncoder().encodeToString(pngBytes);
return callOllama(base64);
}
private static String callOllama(String base64Image) throws Exception {
String json = "{"
+ "\"model\":\"" + OLLAMA_MODEL + "\","
+ "\"messages\":["
+ " {"
+ " \"role\":\"user\","
+ " \"content\":\"识别图中的验证码文字,只返回文字内容,不要有其他解释\","
+ " \"images\":[\"" + base64 + "\"]"
+ " }"
+ "]"
+ "}";
+ "\"model\":\"" + OLLAMA_MODEL + "\","
+ "\"messages\":["
+ " {"
+ " \"role\":\"user\","
+ " \"content\":\"识别图中的验证码文字,只返回文字内容,不要有其他解释\","
+ " \"images\":[\"" + base64Image + "\"]"
+ " }"
+ "]"
+ "}";
URL url = new URL(OLLAMA_URL + "/api/chat");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
@ -376,20 +395,20 @@ public class EtsScraper {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
String response = sb.toString();
// Parse "content":"..." from the JSON response
// Parse "content":"..." from the JSON response
int contentIdx = response.indexOf("\"content\":");
if (contentIdx >= 0) {
int start = response.indexOf('"', contentIdx + 10) + 1;
int end = response.indexOf('"', start);
if (start > 0 && end > start) {
return response.substring(start, end).trim();
}
}
}
}
return null;
} finally {
} finally {
conn.disconnect();
}
}
}
}
}
}

View File

@ -0,0 +1,79 @@
package com.ets.scraper;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class EtsScraperTest {
@TempDir
Path tempDir;
@Test
void testGifToPngConversion() throws Exception {
Path gifPath = tempDir.resolve("captcha.png");
// Copy the actual captcha (GIF stored as .png) to temp
Files.copy(
Path.of("screenshots/captcha.png").toAbsolutePath(),
gifPath
);
byte[] imageBytes = Files.readAllBytes(gifPath);
ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes);
BufferedImage gifImage = ImageIO.read(bais);
assertNotNull(gifImage, "GIF should be readable by ImageIO");
assertTrue(gifImage.getWidth() > 0, "Image should have positive width");
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
ImageIO.write(gifImage, "png", pngOut);
byte[] pngBytes = pngOut.toByteArray();
assertTrue(pngBytes.length > 0, "PNG output should not be empty");
// Verify converted PNG is valid
ByteArrayInputStream bais2 = new ByteArrayInputStream(pngBytes);
BufferedImage pngImage = ImageIO.read(bais2);
assertNotNull(pngImage, "Converted PNG should be readable");
}
@Test
void testGifToPngProducesValidPng() throws Exception {
byte[] gifBytes = Files.readAllBytes(Path.of("screenshots/captcha.png").toAbsolutePath());
ByteArrayInputStream bais = new ByteArrayInputStream(gifBytes);
BufferedImage image = ImageIO.read(bais);
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
ImageIO.write(image, "png", pngOut);
// PNG header: 89 50 4E 47 0D 0A 1A 0A
byte[] pngHeader = pngOut.toByteArray();
// bytes are signed in Java, mask with & 0xFF
assertEquals(0x89 & 0xFF, pngHeader[0] & 0xFF, "PNG magic number");
assertEquals(0x50 & 0xFF, pngHeader[1] & 0xFF, "P");
assertEquals(0x4E & 0xFF, pngHeader[2] & 0xFF, "N");
assertEquals(0x47 & 0xFF, pngHeader[3] & 0xFF, "G");
}
@Test
void testBase64Encoding() throws Exception {
byte[] imageBytes = Files.readAllBytes(Path.of("screenshots/captcha.png").toAbsolutePath());
String base64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
assertNotNull(base64);
assertTrue(base64.length() > 0, "Base64 should not be empty");
assertFalse(base64.contains("\n"), "Base64 should be single line");
// Verify roundtrip
byte[] decoded = java.util.Base64.getDecoder().decode(base64);
assertArrayEquals(imageBytes, decoded, "Base64 roundtrip should match original");
}
}

Binary file not shown.

View File

@ -0,0 +1 @@
com/ets/scraper/EtsScraper.class

View File

@ -0,0 +1 @@
/Users/niko/workspace/ets/ets-playwright/src/main/java/com/ets/scraper/EtsScraper.java

View File

@ -0,0 +1 @@
com/ets/scraper/EtsScraperTest.class

View File

@ -0,0 +1 @@
/Users/niko/workspace/ets/ets-playwright/src/test/java/com/ets/scraper/EtsScraperTest.java

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuite xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report.xsd" version="3.0.2" name="com.ets.scraper.EtsScraperTest" time="0.048" tests="3" errors="0" skipped="0" failures="0">
<properties>
<property name="java.specification.version" value="25"/>
<property name="sun.jnu.encoding" value="UTF-8"/>
<property name="java.class.path" value="/Users/niko/workspace/ets/ets-playwright/target/test-classes:/Users/niko/workspace/ets/ets-playwright/target/classes:/Users/niko/.m2/repository/com/microsoft/playwright/playwright/1.55.0/playwright-1.55.0.jar:/Users/niko/.m2/repository/com/google/code/gson/gson/2.12.1/gson-2.12.1.jar:/Users/niko/.m2/repository/com/google/errorprone/error_prone_annotations/2.36.0/error_prone_annotations-2.36.0.jar:/Users/niko/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/niko/.m2/repository/com/microsoft/playwright/driver/1.55.0/driver-1.55.0.jar:/Users/niko/.m2/repository/com/microsoft/playwright/driver-bundle/1.55.0/driver-bundle-1.55.0.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter/5.12.1/junit-jupiter-5.12.1.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.12.1/junit-jupiter-api-5.12.1.jar:/Users/niko/.m2/repository/org/junit/platform/junit-platform-commons/1.12.1/junit-platform-commons-1.12.1.jar:/Users/niko/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.12.1/junit-jupiter-params-5.12.1.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.12.1/junit-jupiter-engine-5.12.1.jar:/Users/niko/.m2/repository/org/junit/platform/junit-platform-engine/1.12.1/junit-platform-engine-1.12.1.jar:"/>
<property name="java.vm.vendor" value="GraalVM Community"/>
<property name="sun.arch.data.model" value="64"/>
<property name="java.vendor.url" value="https://www.graalvm.org/"/>
<property name="os.name" value="Mac OS X"/>
<property name="java.vm.specification.version" value="25"/>
<property name="sun.java.launcher" value="SUN_STANDARD"/>
<property name="user.country" value="CN"/>
<property name="sun.boot.library.path" value="/Users/niko/.sdkman/candidates/java/25.0.2-graalce/lib"/>
<property name="sun.java.command" value="/Users/niko/workspace/ets/ets-playwright/target/surefire/surefirebooter-20260505000722281_3.jar /Users/niko/workspace/ets/ets-playwright/target/surefire 2026-05-05T00-07-22_250-jvmRun1 surefire-20260505000722281_1tmp surefire_0-20260505000722281_2tmp"/>
<property name="http.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
<property name="jdk.debug" value="release"/>
<property name="surefire.test.class.path" value="/Users/niko/workspace/ets/ets-playwright/target/test-classes:/Users/niko/workspace/ets/ets-playwright/target/classes:/Users/niko/.m2/repository/com/microsoft/playwright/playwright/1.55.0/playwright-1.55.0.jar:/Users/niko/.m2/repository/com/google/code/gson/gson/2.12.1/gson-2.12.1.jar:/Users/niko/.m2/repository/com/google/errorprone/error_prone_annotations/2.36.0/error_prone_annotations-2.36.0.jar:/Users/niko/.m2/repository/org/opentest4j/opentest4j/1.3.0/opentest4j-1.3.0.jar:/Users/niko/.m2/repository/com/microsoft/playwright/driver/1.55.0/driver-1.55.0.jar:/Users/niko/.m2/repository/com/microsoft/playwright/driver-bundle/1.55.0/driver-bundle-1.55.0.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter/5.12.1/junit-jupiter-5.12.1.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter-api/5.12.1/junit-jupiter-api-5.12.1.jar:/Users/niko/.m2/repository/org/junit/platform/junit-platform-commons/1.12.1/junit-platform-commons-1.12.1.jar:/Users/niko/.m2/repository/org/apiguardian/apiguardian-api/1.1.2/apiguardian-api-1.1.2.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter-params/5.12.1/junit-jupiter-params-5.12.1.jar:/Users/niko/.m2/repository/org/junit/jupiter/junit-jupiter-engine/5.12.1/junit-jupiter-engine-5.12.1.jar:/Users/niko/.m2/repository/org/junit/platform/junit-platform-engine/1.12.1/junit-platform-engine-1.12.1.jar:"/>
<property name="sun.cpu.endian" value="little"/>
<property name="user.home" value="/Users/niko"/>
<property name="user.language" value="zh"/>
<property name="java.specification.vendor" value="Oracle Corporation"/>
<property name="java.version.date" value="2026-01-20"/>
<property name="java.home" value="/Users/niko/.sdkman/candidates/java/25.0.2-graalce"/>
<property name="file.separator" value="/"/>
<property name="basedir" value="/Users/niko/workspace/ets/ets-playwright"/>
<property name="java.vm.compressedOopsMode" value="Non-zero disjoint base"/>
<property name="line.separator" value="&#10;"/>
<property name="java.vm.specification.vendor" value="Oracle Corporation"/>
<property name="java.specification.name" value="Java Platform API Specification"/>
<property name="apple.awt.application.name" value="ForkedBooter"/>
<property name="surefire.real.class.path" value="/Users/niko/workspace/ets/ets-playwright/target/surefire/surefirebooter-20260505000722281_3.jar"/>
<property name="user.script" value="Hans"/>
<property name="sun.management.compiler" value="HotSpot 64-Bit Tiered Compilers"/>
<property name="ftp.nonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
<property name="java.runtime.version" value="25.0.2+10-jvmci-b01"/>
<property name="user.name" value="niko"/>
<property name="stdout.encoding" value="UTF-8"/>
<property name="path.separator" value=":"/>
<property name="os.version" value="26.4.1"/>
<property name="java.runtime.name" value="OpenJDK Runtime Environment"/>
<property name="file.encoding" value="UTF-8"/>
<property name="java.vm.name" value="OpenJDK 64-Bit Server VM"/>
<property name="java.vendor.version" value="GraalVM CE 25.0.2+10.1"/>
<property name="localRepository" value="/Users/niko/.m2/repository"/>
<property name="java.vendor.url.bug" value="https://github.com/oracle/graal/issues"/>
<property name="java.io.tmpdir" value="/var/folders/7t/3nly0x6s3dn7dbc8klvwd_3r0000gn/T/"/>
<property name="java.version" value="25.0.2"/>
<property name="user.dir" value="/Users/niko/workspace/ets/ets-playwright"/>
<property name="os.arch" value="aarch64"/>
<property name="java.vm.specification.name" value="Java Virtual Machine Specification"/>
<property name="native.encoding" value="UTF-8"/>
<property name="java.library.path" value="/Users/niko/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:."/>
<property name="java.vm.info" value="mixed mode, sharing"/>
<property name="stderr.encoding" value="UTF-8"/>
<property name="java.vendor" value="GraalVM Community"/>
<property name="java.vm.version" value="25.0.2+10-jvmci-b01"/>
<property name="stdin.encoding" value="UTF-8"/>
<property name="sun.io.unicode.encoding" value="UnicodeBig"/>
<property name="socksNonProxyHosts" value="local|*.local|169.254/16|*.169.254/16"/>
<property name="java.class.version" value="69.0"/>
</properties>
<testcase name="testGifToPngProducesValidPng" classname="com.ets.scraper.EtsScraperTest" time="0.033"/>
<testcase name="testGifToPngConversion" classname="com.ets.scraper.EtsScraperTest" time="0.004"/>
<testcase name="testBase64Encoding" classname="com.ets.scraper.EtsScraperTest" time="0.001"/>
</testsuite>

View File

@ -0,0 +1,4 @@
-------------------------------------------------------------------------------
Test set: com.ets.scraper.EtsScraperTest
-------------------------------------------------------------------------------
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.048 s -- in com.ets.scraper.EtsScraperTest