[트위터] 데이터 수집기
* 전부 Java로 구현함.
* tweepy와 커스텀 파이썬 크롤러는 API와 속도 차이가 너무 커서 포기함.
* 트위터는 bearerToken을 받아서 인증해야 API 사용 가능
* 아래는 API 호출 예시
static String bearerToken = "AAAAAAAAAAAAAAAAAAAAAFJoWQEAAAAAtGXgIPTy1xrcTE5sXcVLmRU1KZ8%3DgdvzO1icW2UsKz0r6srlwfN8Cbu1Cl6eYLLKrgQUmBpY01zG0O";
static HttpClient httpClient = HttpClients.custom()
.setDefaultRequestConfig(RequestConfig.custom()
.setCookieSpec(CookieSpecs.STANDARD).build())
.build();
//요청 보낼 API URI
URIBuilder uriBuilder = new URIBuilder("https://api.twitter.com/2/users");
//쿼리 파라미터 자료형(실제 URI에 쿼리로 추가된다)
ArrayList<NameValuePair> queryParameters;
queryParameters = new ArrayList<>();
//add로 특정 Name의 Value를 추가. 아래는 ids라는 name에 사용자 id 배열을 지정한다.
queryParameters.add(new BasicNameValuePair("ids", String.join(",", ids)));
//user.fields라는 name에 username을 지정. user.fields 중 username을 얻어오겠다는 의미이다.
queryParameters.add(new BasicNameValuePair("user.fields", "username"));
uriBuilder.addParameters(queryParameters);
HttpGet httpGet = new HttpGet(uriBuilder.build());
//HTTP GET 메세지의 Authorization 헤더에 bearerToken을 지정한다.
httpGet.setHeader("Authorization", String.format("Bearer %s", bearerToken));
httpGet.setHeader("Content-Type", "application/json");
//http GET 요청을 보내고 그 결과를 response에 담음.
HttpResponse response = httpClient.execute(httpGet);
//response deserializing
HttpEntity entity = response.getEntity();
//빈게 아니라면 UTF-8 인코딩 문자열로 변환
if (null != entity) {
userResponse = EntityUtils.toString(entity, "UTF-8");
}
//return 할 결과물
String[] ret = null;
if(userResponse!=null) {
//응답 메세지를 JSON 객체화 및 data의 JSONArray를 추출함.
//data와 message 두 개 존재했던 걸로 기억함.
JSONArray res = new JSONObject(userResponse).getJSONArray("data");
ret = new String[res.length()];
//JSONArray인 data 내 각 JSON 객체마다 username을 추출함.
for(int i=0;i<res.length();i++) {
ret[i] = res.getJSONObject(i).getString("username");
}
}
return ret;
* 데이터 개요
구분 | 개수 |
사용자 수 | 82,410 |
관계 수 | 6,641,814 |
트윗 수 | 17,108,019 |
연령 범위 | 1~15년 |
트윗 수집 기간 | 2021/09/09~2021/12/09 |
* 비공개 및 location이 확인되지 않는 사용자는 수집하지 않으므로 비영어권 사용자는 거의 없음.
* 구현 기능 및 부가 설명
이하 URIBuilder, queryParameters 등 리퀘스트마다 공통되는 부가 코드는 생략
1. getUserName(String[] ids)
public static String[] getUserName(String[] ids) throws IOException, URISyntaxException {}
String[] ids의 사용자들의 username을 얻어오는 함수이다. ids는 사용자 식별자이며 username은 사용자명이다. 위 예시 참고.
2. verifyID(String id)
public static boolean verifyID(String id) throws IOException, URISyntaxException {
if(id==null)
return false;
String userResponse = null;
JSONObject res;
//URIBuilder, queryParameters 등 공통되는 부가 코드는 생략
queryParameters.add(new BasicNameValuePair("ids", id));
queryParameters.add(new BasicNameValuePair("user.fields", "created_at,description,pinned_tweet_id"));
res = new JSONObject(userResponse);
try {
if(res.getJSONArray("errors").getJSONObject(0).getString("title").equals("Not Found Error"))
return false;
}catch(Exception e) {
return true;
}
}
매개변수로 주어진 id가 트위터 상 실제 존재하는 id인지 확인하는 함수이다.
3. getFriends(String id)
public static String[] getFriends(String id) throws Exception {
System.out.println("Getting friends of "+id);
String userResponse = null;
String[] ret;
URIBuilder uriBuilder = new URIBuilder("https://api.twitter.com/1.1/friends/ids.json");
queryParameters.add(new BasicNameValuePair("id", id));
//id가 자료형 범위 밖일 경우를 대비해 String으로 반환하도록 요청하는 헤더
queryParameters.add(new BasicNameValuePair("stringify_ids", "true"));
//queryParameters.add(new BasicNameValuePair("count", "100"));
JSONArray res = null;
JSONObject obj = new JSONObject(userResponse);
System.out.println();
res = (JSONArray) obj.get("ids");
ret = new String[res.length()];
Iterator<Object> iterator = res.iterator();
int i=0;
while(iterator.hasNext()) ret[i++] = (String) iterator.next();
return ret;
}
id에 해당되는 사용자가 팔로우하는 사용자들의 id들을 반환하는 함수이다. 기본 5000명씩이었던 것으로 기억.
4. multiUsers(String[] ids)
public static String[] multiUsers(String[] ids) throws IOException, URISyntaxException {
if(ids==null) return null;
String userResponse = null;
String[] ret;
//매개변수 ids를 리퀘스트 크기 제한에 맞춰 100개씩 요청하도록 분할
if(ids.length>100) {
List<String> tmp = (List<String>)Arrays.asList(ids);
List<String> result = new ArrayList<>();
List<List<String>> chunked_tmp = chunkedLists(tmp, 100);
for(List<String> lst:chunked_tmp) {
String[] arr = lst.toArray(new String[0]);
String[] test = multiUsers(arr);
List<String> smth = null;
if(test!=null) {
smth = Arrays.asList(test);
}
if(smth!=null) result.addAll(smth);
}
//안에 new String[0] 넣어줘야 Object[]에서 String[]으로 변환
//아래가 이 함수의 최종 결과를 반환하는 부분이다.
return result.toArray(new String[0]);
}
URIBuilder uriBuilder = new URIBuilder("https://api.twitter.com/2/users");
queryParameters.add(new BasicNameValuePair("ids", String.join(",", String.join(",", ids))));
queryParameters.add(new BasicNameValuePair("user.fields", "created_at,description,pinned_tweet_id"));
JSONArray res = null;
JSONObject obj = new JSONObject(userResponse);
try {
//json 배열이 data랑 error 항목으로 구성돼서 data만 참고하면 없는 사용자를 참고하는 일은 없음.
res = obj.getJSONArray("data");
System.out.println(obj);
}catch(Exception e) {
System.out.println("<Exception>\n"+obj);
//e.printStackTrace();
return null;
}
ret = new String[res.length()];
for(int i=0;i<res.length();i++) ret[i] = res.getJSONObject(i).getString("id");
return ret;
}
주어진 목록의 id들 중 존재하지 않는 id들은 제거하고 존재하는 id들만 남겨서 반환하는 함수이다.
5. chunkedLists(List<T> list, final int chunkSize)
public static <T> List<List<T>> chunkedLists(List<T> list, final int chunkSize) {
if (list == null)
throw new IllegalArgumentException("Input list must not be null");
if (chunkSize <= 0)
throw new IllegalArgumentException("Chunk Size must be > 0");
List<List<T>> subLists = new ArrayList<List<T>>();
final int listSize = list.size();
for (int i = 0; i < listSize; i += chunkSize)
subLists.add(new ArrayList<T>(list.subList(i, Math.min(listSize, i + chunkSize))));
return subLists;
}
리스트를 chunkSize 크기의 부분 리스트들로 분할하는 함수이다.
6. getUsers(String[] usernames, String fileName)
public static String[] getUsers(String[] usernames, String fileName) throws IOException, URISyntaxException, InterruptedException {
String userResponse = null;
String[] ret;
FileWriter myWriter = new FileWriter("C:/Users/PC/Desktop/"+fileName+".txt");
//chunkify id list by 100, since API only allows 100 for each request.
if(usernames.length>100) {
List<String> tmp = (List<String>)Arrays.asList(usernames);
List<String> result = new ArrayList<>();
List<List<String>> chunked_tmp = chunkedLists(tmp, 100);
for(List<String> lst:chunked_tmp) {
String[] arr = lst.toArray(new String[0]);
System.out.println("Array: "+Arrays.toString(arr));
String[] test = getUsers(arr, fileName);
List<String> smth = null;
if(test!=null) {
smth = Arrays.asList(test);
}else {
test = getUsers(arr, fileName);
if(test == null) continue;
}
if(smth!=null) result.addAll(smth);
}
//id와 생성일자 매핑 함수
System.out.println("Printing hashmap.");
System.out.println("Hashmap Size:"+hm.keySet().size());
for(String key:hm.keySet()) {
myWriter.write(key+" "+hm.get(key)+"\n");
}
//'id 생성일자'를 파일에 기록
myWriter.flush();
myWriter.close();
return result.toArray(new String[0]);
}
queryParameters.add(new BasicNameValuePair("ids", String.join(",", String.join(",", usernames))));
queryParameters.add(new BasicNameValuePair("user.fields", "created_at,description,pinned_tweet_id"));
JSONArray res = null;
JSONObject obj = new JSONObject(userResponse);
System.out.println(obj);
try {
//json 배열이 data랑 error 항목으로 구성돼서 data만 참고하면 없는 사용자를 참고하는 일은 없음.
res = obj.getJSONArray("data");
}catch(Exception e) {
e.printStackTrace();
//API 시간 제한으로 대기
Thread.sleep(15*60*1000+1000);
return null;
}
ret = new String[res.length()];
for(int i=0;i<res.length();i++) {
String date = res.getJSONObject(i).getString("created_at").substring(0, 4);
hm.put(res.getJSONObject(i).getString("id"), date);
ret[i] = date;
}
return ret;
}
사용자 id와 생성일자를 매핑하여 text file에 기록하는 함수이다.
7. getTweets(String userId, String nextToken, CSVWriter csv)
public static String getTweets(String userId, String nextToken, CSVWriter csv) throws IOException, URISyntaxException, InterruptedException {
String tweetResponse = null;
boolean sw = false;
URIBuilder uriBuilder = new URIBuilder(String.format("https://api.twitter.com/2/users/%s/tweets", userId));
queryParameters = new ArrayList<>();
queryParameters.add(new BasicNameValuePair("max_results", "100"));
queryParameters.add(new BasicNameValuePair("start_time", "2021-09-04T00:00:00Z"));
queryParameters.add(new BasicNameValuePair("end_time", "2021-12-04T00:00:00Z"));
queryParameters.add(new BasicNameValuePair("tweet.fields", "created_at,public_metrics"));
queryParameters.add(new BasicNameValuePair("exclude", "replies"));
if(nextToken!=null) queryParameters.add(new BasicNameValuePair("pagination_token", nextToken));
while(true) {
HttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();
if (null != entity) {
tweetResponse = EntityUtils.toString(entity, "UTF-8");
}
JSONArray res = null;
JSONObject obj = new JSONObject(tweetResponse);
try {
//no tweets
if(obj.getJSONObject("meta").getInt("result_count")==0) return null;
res = obj.getJSONArray("data");
}catch(Exception e) {
if(obj.has("errors")) {
String tmp = (String)((JSONObject)obj.getJSONArray("errors").get(0)).get("detail");
if(((tmp.equals("Sorry, you are not authorized to see the user with id: ["+userId+"].")
|| tmp.equals("Could not find user with id: ["+userId+"].")
|| tmp.equals("User has been suspended: ["+userId+"].")))) return null;
}
//True if 한차례 대기했음 else False
if(sw == false) {
System.out.println("Thread sleeps until "+LocalDateTime.now().plusMinutes(15).plusSeconds(10));
Thread.sleep(15*60*1000+1000);
}
else return null;
sw = true;
}
//트윗 정보 하나씩 입력해나감.
for(int i=0;i<res.length();i++) {
JSONObject object = res.getJSONObject(i);
JSONObject metrics = object.getJSONObject("public_metrics");
csv.writeNext(new String[]{userId,object.getString("created_at"),object.getString("id"),object.getString("text").replaceAll("[\\n\\t,]", "").replaceAll(",", " "),String.valueOf(metrics.getInt("retweet_count")),String.valueOf(metrics.getInt("like_count")),String.valueOf(metrics.getInt("reply_count")),String.valueOf(metrics.getInt("quote_count"))});
}
csv.flush();
//fetch next token for pagination.
try{nextToken = obj.getJSONObject("meta").getString("next_token");}
catch(Exception e) {return null;}
//이 부분을 return 하는 걸로 바꾸고 다음 호출 때 넘기는 식으로 구현 바람.
//재귀 깊이가 계속 쌓여감.
if(nextToken!=null) getTweets(userId, nextToken, csv);
return null;
}
}