Search

Java - PTT 文章代碼(AID)轉換為 URL

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