2015-12-26
5:38 PM
因為開發經驗第二集要花比較多時間撰寫
剛好最近有用到 AID(文章代碼) 轉換成 URL 網址的方法
寫了一個轉換工具 AidConverter
可以轉換 AID 為 URL 或是 檔案名稱
也可以將 URL 或 檔案名稱 轉換為 AID
就決定先寫這篇教學文章了
PTT 的原始碼是用 C 語言寫的 而且檔名很可怕!!!
但我只碰過 C++ 而且那也已經是N年前的事了(遙想當年...)
我懶得複習也懶得去看那幾段程式碼到底在做什麼
就用硬轉的方式把那幾段轉成 Java 語言
所以這段教學不會有什麼說明 打算放放原始碼就了事...
雖然測試回傳結果都是正確的 但只測過十幾篇所以記得自己加一些錯誤處理
如果有大師願意修正為更好的版本也請不要客氣囉~
AidConverter
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.ihad.ptt.model.bean.AidBean;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* 文章編號與檔案名稱轉換工具<br />
* 將符合此格式的 [a-zA-Z-_]{8} 文章編號轉換為檔案名稱 或是 URL<br />
* 範例:<br />
* 1MVYyFDv -> M.1451110159.A.379 -> https://www.ptt.cc/bbs/Gossiping/M.1451110159.A.379.html<br /><br />
* 或是將符合 [M|G].[unsigned_integer].A.[HEX{3}] 格式的檔案名稱轉換為文章編號<br />
* 若為 URL 則將轉換為 {@link AidBean}, 其中包含看板名與文章編號<br />
* 範例:<br />
* https://www.ptt.cc/bbs/Gossiping/M.1451110159.A.379.html -> M.1451110159.A.379 -> 1MVYyFDv
*/
public class AidConverter {
private static final String DOMAIN_URL = "https://www.ptt.cc/bbs/";
private static final String FILE_EXT = ".html";
private static String aidTable = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
private static BiMap<Character, Long> table = tableInitializer();
/**
* 建立文章編號字元 Map, 方便取得對應數值
* @return
* 文章編號字元 Map
*/
private static BiMap<Character, Long> tableInitializer(){
BiMap<Character, Long> table = HashBiMap.create();
long index = 0;
int size = aidTable.length();
for( int i = 0; i < size; i++ ){
table.forcePut( aidTable.charAt(i), index );
index ++;
}
return table;
}
/**
* 將檔案名稱轉換為˙數字型態的文章序號
* @param fn 檔案名稱
* @return
* 數字型態的文章序號, 若檔案名稱格式不符將回傳 0<br />
* 轉換後的文章編號將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
* 範例: M.1451100858.A.71E
*/
private static long fn2aidu(String fn){
long aidu = 0;
long type = 0;
long v1 = 0;
long v2 = 0;
if( fn == null ) return 0;
List<String> fnList = Splitter.on(".").omitEmptyStrings().trimResults().splitToList(fn);
if( fnList.size() != 4 ) return 0;
String typeString = fnList.get(0);
String v1String = fnList.get(1);
String v2String = fnList.get(3);
if( !fnList.get(2).equals("A") ) return 0;
if( !StringUtils.isNumeric(v1String) || v1String.length() != 10 ) return 0;
switch (typeString){
case "M":
type = 0;
break;
case "G":
type = 1;
break;
default:
return 0;
}
v1 = Long.parseLong( v1String );
v2 = Long.parseLong( v2String, 16 );
aidu = ((type & 0xf) << 44) | ((v1 & 0xffffffffL) << 12) | (v2 & 0xfff);
return aidu;
}
/**
* 將數字型態的文章序號轉換為字串型態的文章編號
* @param aidu 數字型態之文章序號
* @return
* 轉換後的文章編號將符合 [a-zA-Z-_]{8}<br />
* 範例: 1MVWgwSU
*/
private static String aidu2aidc(long aidu){
int size = table.size();
BiMap<Long, Character> inverseTable = table.inverse();
StringBuffer stringBuffer = new StringBuffer();
while( stringBuffer.length() < 8 ){
long v = aidu % size;
if( !inverseTable.containsKey( v ) ) return null;
stringBuffer.insert( 0, inverseTable.get( v ) );
aidu = aidu / size;
}
return stringBuffer.toString();
}
/**
* 將文章編號轉換為數字型態的文章序號
* @param aid 文章編號
* @return
* 數字型態的文章序號
*/
private static long aidc2aidu(String aid){
char[] aidChars = aid.toCharArray();
long aidu = 0;
for( char aidChar : aidChars ){
if( aidChar == '@' ) break;
if( !table.containsKey(aidChar) ) return 0;
long v = table.get(aidChar);
aidu = aidu << 6;
aidu = aidu | (v & 0x3f);
}
return aidu;
}
/**
* 將文章序號(數字型態)轉換為檔案名稱
* @param aidu 文章序號(數字型態)
* @return
* 轉換後的檔案名稱, 格式將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
* 最後的16進位表示法若未滿3個字將以0從左邊開始補齊<br />
* 範例: M.1451100858.A.71E
*/
private static String aidu2fn(long aidu){
long type = ((aidu >> 44) & 0xf);
long v1 = ((aidu >> 12) & 0xffffffffL);
long v2 = (aidu & 0xfff);
// casting to unsigned
// v1 = v1 & 0xffffffffL;
String hex = Long.toHexString(v2).toUpperCase();
return ((type == 0) ? "M" : "G") + "." + v1 + ".A." + StringUtils.leftPad(hex, 3, "0");
}
/**
* 將文章編號轉換為檔案名稱
* @param aid 文章編號
* @return
* 轉換後的檔案名稱, 格式將符合 [M|G].[unsigned_integer].A.[HEX{3}]<br />
* 最後的16進位表示法若未滿3個字將以0從左邊開始補齊<br />
* 範例: M.1451100858.A.71E
*/
public static String aidToFileName(String aid){
return aidu2fn( aidc2aidu(aid) );
}
/**
* 將文章編號轉換為 WEB 版 URL
* @param boardTitle 文章所屬看板名稱
* @param aid 文章編號
* @return
* WEB 版的完整 URL
*/
public static String aidToUrl(String boardTitle, String aid){
if( boardTitle.isEmpty() || aid.isEmpty() ) return "";
return DOMAIN_URL + boardTitle + "/" + aidToFileName(aid) + FILE_EXT;
}
/**
* 將檔案名稱(也就是 URL 的最後一段 不包含副檔名)轉換為文章編號
* @param fileName 檔案名稱
* @return
* 轉換後的文章編號, 若檔案名稱格式不符則將回傳 null
*/
public static String fileNameToAid(String fileName){
return aidu2aidc( fn2aidu(fileName) );
}
/**
* 將 URL 轉換為 AID 物件
* @param url PTT WEB 版的 URL
* @return
* 物件內包含 文章編號 與 看板名
* @see AidBean
*/
public static AidBean urlToAid(String url){
List<String> urlList = Splitter.on("/").omitEmptyStrings().trimResults().splitToList(url);
if( urlList.size() < 4 ) return null;
String boardTitle = urlList.get(3);
String fileName = urlList.get(4).replaceAll("\\.(html|htm)", "");
String aid = fileNameToAid( fileName );
return AidBean.Builder.anAidBean()
.withBoardTitle( boardTitle )
.withAid( aid )
.build();
}
}
AidBean
public class AidBean{
// 看板名稱
private String boardTitle;
// 文章編號
private String aid;
public String getBoardTitle() {
return boardTitle;
}
public void setBoardTitle(String boardTitle) {
this.boardTitle = boardTitle;
}
public String getAid() {
return aid;
}
public void setAid(String aid) {
this.aid = aid;
}
public static class Builder {
private String boardTitle;
private String aid;
private Builder() {
}
public static Builder anAidBean() {
return new Builder();
}
public Builder withBoardTitle(String boardTitle) {
this.boardTitle = boardTitle;
return this;
}
public Builder withAid(String aid) {
this.aid = aid;
return this;
}
public Builder but() {
return anAidBean().withBoardTitle(boardTitle).withAid(aid);
}
public AidBean build() {
AidBean aidBean = new AidBean();
aidBean.setBoardTitle(boardTitle);
aidBean.setAid(aid);
return aidBean;
}
}
}
各項資料連結
Java - PTT 批踢踢實業坊 Client 軟體開發經驗(1) - 前置準備
PTT 原始碼
No comments:
Post a Comment