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