관련지식
node.js, request, cheerio

아래의 화면은 구글 이미지 검색 결과입니다. ‘이미지로 검색’ 아닙니다. ‘이미지를 검색’ 입니다.

이미지 검색기능은 구글에서 제공하는 API를 통해서 사용할수도 있지만 여기서는 API를 사용하지 않고 웹 검색결과를 가공하는 기능을 만들어보겠습니다. 아래와 같은 형태로 호출할수 있도록 만들것입니다.

  1. (async () => {
  2. let items = await search('바다');
  3. console.log(items.length);
  4. console.log(items[0]);
  5. })();

search() 함수

브라우저에서 이미지 검색을 해보면 굉장히 많은 파라미터가 붙지만, 실제로는 tbm, tbs, q 세가지 파라미터만 있으면 됩니다. 파라미터 q 에는 검색어를 입력하면 되고 파라미터 tbs에는 검색 조건등을 지정할수 있습니다. 제가 직접 확인한 검색조건 몇가지는 주석으로 남겼고, 더 많은 조건은 https://stenevang.wordpress.com/2013/02/22/google-advanced-power-search-url-request-parameters/ 에서 확인해보실수 있습니다. 다만 오래된 게시물이라 현재와 다를수 있습니다.

  1. let google = 'https://www.google.com';
  2. async function search(keyword) {
  3. keyword = encodeURIComponent(keyword);
  4. //지난1일 qdr:d
  5. //지난1주 qdr:w
  6. //지난1개월 qdr:m
  7. //지난1년 qdr:y
  8. //큰사이즈 이미지 isz:l
  9. //중간사이즈 이미지 isz:m
  10. //얼굴 itp:face
  11. //사진 itp:photo
  12. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,isz:l,itp:photo`;
  13. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=isz:l,itp:photo`;
  14. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,isz:l`;
  15. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:photo`;
  16. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:face`;
  17. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,isz:l,itp:face`;
  18. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:photo`;
  19. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:photo`;
  20. let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:y`;
  21. let bodyHtml = await req(url);
  22. let data = parse(bodyHtml);
  23. let itemList = data.items;
  24. //다른 페이지 검색
  25. while(data.pages.length) {
  26. url = google + data.pages.shift();
  27. let nextHtml = await req(url);
  28. let nextData = parse(nextHtml);
  29. itemList = itemList.concat(nextData.items);
  30. }
  31. return itemList;
  32. }

req() 함수

req() 함수에서 URL호출시 사용하는 request 객체가 비동기 함수로 제공되기 때문에 Promise를 적용하였고, 이후 함수들이 async/await 키워드를 사용하게 되었습니다.

이 소스에서는 아무런 헤더값을 사용하지 않았지만 user-agent를 사용할때와 안할때 검색 결과가 달라집니다. 파싱 방법도 달라진다는 것을 참고하시면 되겠습니다. 또한 로그인된 쿠키값을 사용했을때도 검색결과가 달라질수 있습니다. 예를들어 19금 이상의 검색 결과가 필터링 될것이냐, 안될것이냐의 차이가 발생하겠죠.

  1. function req(url) {
  2. return new Promise(function(resolve, reject) {
  3. request.get({
  4. url : url,
  5. }, function(err, reponse, body) {
  6. if(err) {
  7. reject(err);
  8. return;
  9. }
  10. let bodyHtml = body;
  11. bodyHtml = bodyHtml.replace(/[\w\W]*<body/g, '<body');
  12. console.info(`request success ${url}`);
  13. resolve(bodyHtml);
  14. });
  15. });
  16. }

parse() 함수

parse() 함수에서는 cheerio 패키지를 이용하여 링크, 이미지, 제목 정보와 페이지 정보를 추출합니다. 공식 API와는 다르게 HTML의 태그 정보를 분석하여 추출하므로 만약 구글에서 퍼블리싱을 수정한다면 이 소스도 수정되어야 합니다.

  1. function parse(html) {
  2. let items = [];
  3. let pages = [];
  4. let $ = cheerio.load(html);
  5. let $tds = $('.images_table tbody td');
  6. console.info(`parse1 start : ${$tds.length}`);
  7. for(let i = 0; i < $tds.length; i++) {
  8. let $td = $tds.eq(i);
  9. let link = $td.find("a").prop('href').replace(/(\/url\?q=)/g, ''); // delete => /url?q=
  10. let imageSrc = $td.find("a img").prop('src');
  11. let desc = $td.html();
  12. desc = desc.replace(/(<a[\w\W]*<\/a>|<cite[\w\W]*<\/cite>|<b>[\w\W]*<\/b>)/g, '').split('<br>');
  13. let obj = {
  14. link : decodeURIComponent(link),
  15. imageSrc : decodeURIComponent(imageSrc),
  16. title : $td.html(desc[2]).text()
  17. };
  18. items.push(obj);
  19. }
  20. let $anchors = $('#foot table a');
  21. for(let i = 0; i < $anchors.length && i < 9; i++) {
  22. let a = $anchors.eq(i);
  23. pages.push(a.prop('href'));
  24. }
  25. return {
  26. items : items,
  27. pages : pages
  28. };
  29. }

장점 & 단점

앞서 적은것처럼 HTML결과를 파싱하는 것이므로 결과값이 달라질 경우 소스를 수정해야 합니다. 그리고 제한된 정보만 알아낼수 있죠. 하지만 이미지 검색만을 하기 위해 API 연동을 하지 않아도 되므로 훨씬 간단하게 사용할수가 있습니다. 위의 코드는 복잡한 로직이 없으므로, 필요하신 경우 파이썬이나 다른 코드로 어렵지 않게 포팅하실수 있을겁니다.

최종소스

  1. const request = require('request');
  2. const cheerio = require('cheerio');
  3. function req(url) {
  4. return new Promise(function(resolve, reject) {
  5. request.get({
  6. url : url,
  7. }, function(err, reponse, body) {
  8. if(err) {
  9. reject(err);
  10. return;
  11. }
  12. let bodyHtml = body;
  13. bodyHtml = bodyHtml.replace(/[\w\W]*<body/g, '<body');
  14. console.info(`request success ${url}`);
  15. resolve(bodyHtml);
  16. });
  17. });
  18. }
  19. function parse(html) {
  20. let items = [];
  21. let pages = [];
  22. let $ = cheerio.load(html);
  23. let $tds = $('.images_table tbody td');
  24. console.info(`parse1 start : ${$tds.length}`);
  25. for(let i = 0; i < $tds.length; i++) {
  26. let $td = $tds.eq(i);
  27. let link = $td.find("a").prop('href').replace(/(\/url\?q=)/g, ''); // delete => /url?q=
  28. let imageSrc = $td.find("a img").prop('src');
  29. let desc = $td.html();
  30. desc = desc.replace(/(<a[\w\W]*<\/a>|<cite[\w\W]*<\/cite>|<b>[\w\W]*<\/b>)/g, '').split('<br>');
  31. let obj = {
  32. link : decodeURIComponent(link),
  33. imageSrc : decodeURIComponent(imageSrc),
  34. title : $td.html(desc[2]).text()
  35. };
  36. items.push(obj);
  37. }
  38. let $anchors = $('#foot table a');
  39. for(let i = 0; i < $anchors.length && i < 9; i++) {
  40. let a = $anchors.eq(i);
  41. pages.push(a.prop('href'));
  42. }
  43. return {
  44. items : items,
  45. pages : pages
  46. };
  47. }
  48. let google = 'https://www.google.com';
  49. async function search(keyword) {
  50. keyword = encodeURIComponent(keyword);
  51. //지난1일 qdr:d
  52. //지난1주 qdr:w
  53. //지난1개월 qdr:m
  54. //지난1년 qdr:y
  55. //큰사이즈 이미지 isz:l
  56. //중간사이즈 이미지 isz:m
  57. //얼굴 itp:face
  58. //사진 itp:photo
  59. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,isz:l,itp:photo`;
  60. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=isz:l,itp:photo`;
  61. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,isz:l`;
  62. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:photo`;
  63. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:face`;
  64. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,isz:l,itp:face`;
  65. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:photo`;
  66. // let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:w,itp:photo`;
  67. let url = `${google}/search?q=${keyword}&tbm=isch&tbs=qdr:y`;
  68. let bodyHtml = await req(url);
  69. let data = parse(bodyHtml);
  70. let itemList = data.items;
  71. //다른 페이지 검색
  72. while(data.pages.length) {
  73. url = google + data.pages.shift();
  74. let nextHtml = await req(url);
  75. let nextData = parse(nextHtml);
  76. itemList = itemList.concat(nextData.items);
  77. }
  78. return itemList;
  79. }
  80. (async () => {
  81. let items = await search('바다');
  82. console.log(items.length);
  83. console.log(items[0]);
  84. })();