Halo博客的谷歌收录自动提交
前言
- 在Halo博客的百度定时页面提交一文中已经实现了向百度的主动页面提交,而对于Google平台,实际上并不需要设计类似的功能,一方面Google的基于sitemap的抓取效果已经很好,另一方面,虽然Google也提供了indexing API以提供主动提交的服务,但是需要挂代理才能访问
- 但是为了功能的完整性以及可以使用树莓派直接挂代理访问,于是决定基于Google indexing API实现谷歌收录的自动提交
准备工作
- 实际上,谷歌SEO提供了丰富的文档供站点管理者学习,但是本文仅摘取其中对于indexing API支持的部分,进行简要的介绍
- 全程设置工作需要正常访问谷歌
获取访问令牌
- indexing API使用了OAuth2.0的验证方式,请求该API时需要提供访问令牌,因此第一步,首先在Google Cloud Platform中执行相关设置
进入服务账号页面创建项目
点击创建服务账号
直接点击完成即可,两个可选部分不用管
创建私钥,注意选择JSON类型的私钥
- 执行创建后,私钥文件会下载到本地
Search Console添加网站
- 在Search Console添加网站实际上是验证网站所有权,有多种方法,可参考验证网站所有权
博主自己使用的是域名提供商的方式,比较简单,如下图所示就是验证成功
赋予服务账号所有者状态
- 实际上是向第一步创建的服务账号授予第二步添加的网站的所有权
访问网站站长中心,计入到网站条目中,点击添加所有者
- 要求输入服务账号电子邮件地址,此地址可以从第一步中下载到的私钥中的
client_name
字段中找到
项目构建
建立Gradle工程,配置文件如下所示
plugins { id 'java' id 'application' } group 'xyz.demoli' version '1.0-SNAPSHOT' sourceCompatibility = 1.11 mainClassName="xyz.demoli.Main" repositories { mavenCentral() } application{ applicationDefaultJvmArgs = ['-Duser.timezone=GMT+8'] } dependencies { testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' compile 'com.google.api-client:google-api-client:1.33.0' implementation 'com.google.auth:google-auth-library-oauth2-http:1.3.0' compile 'com.google.apis:google-api-services-indexing:v3-rev20200804-1.32.1' // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.3' implementation 'com.google.code.gson:gson:2.9.0' // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.14.1' // https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.14.1' // https://mvnrepository.com/artifact/org.projectlombok/lombok compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.22' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.22' } test { useJUnitPlatform() }
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.22'
保证gradle项目中lombok的注解可以被正确解析applicationDefaultJvmArgs
参数的设置是为了解决后续服务部署在容器中时日志打印时间不是东八区时区的问题
配置文件
config.properties
如下:prefix=https://blog.demoli.xyz postAPI=%s/api/content/posts?api_access_key=%s&page=%d apiAccessKey=*** proxyURL=192.168.0.137 proxyPort=7890
apiAccessKey
是在Halo博客设置中设定的prefix
是Halo博客的首页访问URLproxy
的两个配置即是代理配置
日志配置文件如下(粗糙的配置):
<?xml version="1.0" encoding="utf-8" ?> <configuration status="INFO"> <appenders> <console name="console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </console> </appenders> <loggers> <root level="INFO"> <appender-ref ref="console"/> </root> </loggers> </configuration>
- 将准备工作中得到的私钥放在项目的
resources
目录下,更名为cred.json
整个工程只有两个核心类
PostScrap
import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; /** * 使用Halo API获取文章链接 */ public class PostScrap { static private String postAPI; static private String apiAccessKey; static private String prefix; // 缓存 static private final Set<String> links = new HashSet<>(); // 注意properties配置文件中字符串不用加引号 static { try (InputStream stream = PostScrap.class.getResourceAsStream("/config.properties")) { Properties properties = new Properties(); properties.load(stream); apiAccessKey = properties.getProperty("apiAccessKey"); prefix = properties.getProperty("prefix"); postAPI = properties.getProperty("postAPI"); } catch (IOException e) { e.printStackTrace(); } } /** * 发起请求获取全部文章链接 * @return */ public static List<String> getPosts() { List<String> res = new ArrayList<>(); OkHttpClient client = new OkHttpClient(); Request initialRequest = new Request.Builder().get().url(String.format(postAPI,prefix,apiAccessKey,0)).build(); try (Response response = client.newCall(initialRequest).execute()) { res = handlePage(response, client); } catch (IOException e) { e.printStackTrace(); } return res; } /** * 处理分页 * @param initialResponse * @param client * @return * @throws IOException */ private static List<String> handlePage(Response initialResponse, OkHttpClient client) throws IOException { JsonObject jsonObject = new Gson().fromJson(initialResponse.body().string(), JsonObject.class); JsonArray array = jsonObject.get("data").getAsJsonObject().get("content").getAsJsonArray(); int pages = jsonObject.get("data").getAsJsonObject().get("pages").getAsInt(); // jsonArray转为List List<String> posts = new ArrayList<>(); for (JsonElement element: array) { posts.add(element.getAsJsonObject().get("fullPath").getAsString()); } // 分页查询 for(int i = 1; i < pages; i++) { Request request = new Request.Builder().get().url(String.format(postAPI,prefix,apiAccessKey,i)).build(); try (Response response = client.newCall(request).execute()) { jsonObject = new Gson().fromJson(response.body().string(), JsonObject.class); array = jsonObject.get("data").getAsJsonObject().get("content").getAsJsonArray(); for (JsonElement element: array) { posts.add(element.getAsJsonObject().get("fullPath").getAsString()); } } catch (IOException e) { e.printStackTrace(); } } // 缓存过滤 return posts.stream().map(content -> prefix + content).filter(links::add).collect( Collectors.toList()); } }
GoogleSubmitter
import com.google.api.client.googleapis.GoogleUtils; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponse; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.indexing.v3.Indexing; import com.google.api.services.indexing.v3.model.UrlNotification; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auth.oauth2.GoogleCredentials; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.Proxy; import java.security.GeneralSecurityException; import java.util.List; import java.util.Properties; import lombok.extern.log4j.Log4j2; /** * 提交谷歌收录 */ @Log4j2 public class GoogleSubmitter { private static GoogleCredentials googleCredentials; private static String proxyURL; private static Integer proxyPort; static { // 加载秘钥 try (InputStream stream = PostScrap.class.getResourceAsStream("/cred.json")) { googleCredentials = GoogleCredentials.fromStream(stream); } catch (IOException e) { e.printStackTrace(); } // 加载配置文件 try (InputStream config = PostScrap.class.getResourceAsStream("/config.properties")) { Properties properties = new Properties(); properties.load(config); proxyURL = properties.getProperty("proxyURL"); proxyPort = Integer.parseInt(properties.getProperty("proxyPort")); } catch (IOException e) { e.printStackTrace(); } } /** * 设置本地代理 * * @return * @throws GeneralSecurityException * @throws IOException */ static HttpTransport newProxyTransport() throws GeneralSecurityException, IOException { NetHttpTransport.Builder builder = new NetHttpTransport.Builder(); builder.trustCertificates(GoogleUtils.getCertificateTrustStore()); builder.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyURL, proxyPort))); return builder.build(); } /** * 提交链接 */ public static void submit() { // 获取待提交链接 List<String> urls = PostScrap.getPosts(); if (urls.size() == 0) { log.info("无新增文章"); return; } try { // 构建indexing服务 // HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); HttpTransport httpTransport = newProxyTransport(); JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); HttpRequestInitializer requestInitializer = new HttpCredentialsAdapter(googleCredentials); Indexing indexing = new Indexing(httpTransport, jsonFactory, requestInitializer); Indexing.UrlNotifications notifications = indexing.urlNotifications(); int count = 0; for (String url : urls) { UrlNotification notification = new UrlNotification(); notification.setUrl(url); // URL_REMOVED 或者 URL_UPDATED notification.setType("URL_UPDATED"); Indexing.UrlNotifications.Publish publish = notifications.publish(notification); HttpResponse response = publish.executeUnparsed(); if (response.getStatusCode() != 200) { log.error("提交失败: {}", url); } else { log.info("提交成功: {}", url); count++; } } log.info("提交成功 {} 条链接", count); } catch (GeneralSecurityException | IOException e) { e.printStackTrace(); } } }
Main
import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) { Executors.newScheduledThreadPool(1) .scheduleWithFixedDelay(GoogleSubmitter::submit, 0, 12, TimeUnit.HOURS); } }
工程部署
- 项目根目录执行
gradle build -x test
将
build/distributions/GoogleSubmit-1.0-SNAPSHOT.tar
拷贝到安装有Java环境的服务器tar xf GoogleSubmit-1.0-SNAPSHOT.tar cd GoogleSubmit-1.0-SNAPSHOT nohup bin/GoogleSubmit > nohup.out &
tail -f nohup.out
查看日志
补充
- 博主是一个Docker容器的究极爱好者,因为使用容器可以保证宿主机环境的”纯净“,所以这里补充使用Docker容器部署服务的方式
首先将项目构建得到的软件包
build/distributions/GoogleSubmit-1.0-SNAPSHOT.tar
拷贝到服务器,解压并重新命名,创建Dockerfiletar xf GoogleSubmit-1.0-SNAPSHOT.tar mkdir -p blogSubmitter/googleSubmitter mv GoogleSubmit-1.0-SNAPSHOT blogSubmitter/googleSubmitter/google cd blogSubmitter/googleSubmitter touch Dockerfile
Dockerfile文件如下:
FROM openjdk:11 COPY . /submitter WORKDIR /submitter # 更改时区 RUN rm -rf /etc/localtime RUN ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime CMD ["nohup","google/bin/GoogleSubmitter"," &"]
创建yaml配置文件,使用Docker Compose构建服务
cd blogSubmitter touch submitter.yaml
version: '3.1' services: blog-google-submitter: build: ./googleSubmitter container_name: blogGoogleSubmitter restart: unless-stopped
- 执行
docker-compose -f submitter.yaml up -d
创建服务
注意事项
- 如果更改了源码,需要重新构建镜像,此时要把之前的镜像删除(应该有更好的解决办法,有待改善,比如使用volume的方式执行挂载)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。