main.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. from selenium import webdriver
  2. from selenium.webdriver.common.by import By
  3. from selenium.webdriver.common.keys import Keys
  4. from webdriver_manager.chrome import ChromeDriverManager
  5. import lxml.html
  6. from lxml import etree
  7. from bs4 import BeautifulSoup
  8. from fake_useragent import UserAgent
  9. from textblob import TextBlob
  10. import time
  11. import re
  12. import csv
  13. import random
  14. from pathlib import Path
  15. import config
  16. from database import DataBase
  17. class Browser:
  18. """ Класс представляющий браузер """
  19. def get_browser(self):
  20. """ Настроить и получить браузер """
  21. user_agent = UserAgent()
  22. user_agent = user_agent.opera
  23. driver = ChromeDriverManager().install()
  24. options = webdriver.ChromeOptions()
  25. options.add_argument(f'--user-agent={user_agent}')
  26. options.add_argument(f'--disable-blink-features=AutomationControlled')
  27. #options.add_argument('--headless')
  28. browser = webdriver.Chrome(
  29. driver,
  30. options=options
  31. )
  32. return browser
  33. def launch_browser(self, url, time=5):
  34. """ Запустить браузер """
  35. self.browser.get(url)
  36. self.browser.maximize_window()
  37. self.browser.implicitly_wait(time)
  38. def close_browser(self):
  39. """ Закрыть браузер """
  40. self.browser.close()
  41. class Parser(Browser):
  42. """ Класс представляющий парсер """
  43. def __init__(self):
  44. self.home_page = 'https://stackoverflow.com/questions/'
  45. self.browser = self.get_browser()
  46. self.launch_browser(url=self.home_page)
  47. self.parsing_process()
  48. def convert_to_bs(self):
  49. """ Получить объект BeautifulsSoup из Selenium """
  50. html_source = self.browser.page_source
  51. page = BeautifulSoup(html_source, 'lxml')
  52. return page
  53. def get_bs_object(self, file_name):
  54. """ Открыть сохранненый файл страницы и вернуть его в виде объекта bs """
  55. page_file = Path('stackoverflow_html/')
  56. page_file = page_file/file_name
  57. with open(page_file, 'r', encoding='utf-8') as file:
  58. text = file.read()
  59. page = BeautifulSoup(text, 'lxml')
  60. return page
  61. def wait(self, t=2.6):
  62. """ Чуть-чуть подождать """
  63. t = random.uniform(1, t)
  64. print(f'Ждем {t}\n')
  65. time.sleep(t)
  66. def write_to_csv(self, data):
  67. """ Записать переданные данные в csv файл """
  68. with open('data.csv', 'a') as file:
  69. writer = csv.writer(file)
  70. def close_banners(self):
  71. """ Закрываем баннеры """
  72. self.browser.find_element(
  73. By.XPATH,
  74. '//div[@class="ff-sans ps-fixed z-nav-fixed ws4 sm:w-auto p32 sm:p16 bg-black-750 fc-white bar-lg b16 l16 r16 js-consent-banner"]//div[@class="grid gs8 ai-stretch fd-column sm:fd-row"]/button[1]'
  75. ).click()
  76. self.browser.implicitly_wait(5)
  77. def parsing_process(self):
  78. """ Весь процесс парсинга """
  79. def go_to_next_page(page_number):
  80. """ Перейти на следующую страницу вопросов """
  81. try:
  82. link = f'https://stackoverflow.com/questions?tab=newest&page={page_number}'
  83. self.launch_browser(url=link)
  84. except:
  85. return 0
  86. page_number = 1
  87. while True:
  88. links_all_questions = self.get_links_all_questions() # Получаем список вопросов со страницы
  89. for i, question_link in enumerate(links_all_questions, 1):
  90. print(f'Вопрос #{i} страницы {page_number}')
  91. print(f'Его ссылка - {question_link}')
  92. self.launch_browser(url=question_link) # Открываем первый вопрос
  93. well_waht = self.get_content_question()
  94. if not well_waht:
  95. print('У этого вопроса нет ответов')
  96. self.wait()
  97. page_number += 1
  98. try:
  99. go_to_next_page(page_number) # Переходим на следующую страницу
  100. except:
  101. print('Такой страницы нет')
  102. break
  103. print(f'\nПарсинг закончен')
  104. return
  105. def get_links_all_questions(self):
  106. """ Получить ссылки на все вопросы """
  107. links = []
  108. temp = ['https://stackoverflow.com/questions/67506798/reformat-dataframe-add-rows-when-condition-is-met']
  109. question_links = self.browser.find_elements(
  110. By.XPATH,
  111. '//div[@id="questions"]/div[@class="question-summary"]//h3/a'
  112. )
  113. for question_link in question_links:
  114. address = question_link.get_attribute('href')
  115. links.append(address)
  116. return temp
  117. def translate_text(self, text):
  118. """ Переводим полученный текст и возвращаем его """
  119. def cut_out_excess(text):
  120. """ Вырезать лишние теги, которые не требуются в вопросе """
  121. result = re.sub(r'<aside.*?>.*?</aside>', '', str(text), flags=re.S)
  122. return result
  123. def cut_code_from_question(text):
  124. """ Получаем все блоки кода из вопроса и возврощаем их """
  125. cut_out_code = re.findall(r'<pre.+?>.+?</pre>|<hr/>|<a.+?>.+?</a>|<code.*?>.*?</code>|<blockquote.*?>.*?</blockquote>', text, flags=re.S)
  126. return cut_out_code
  127. def get_translated_text(text):
  128. """ Переводим текст """
  129. blob = TextBlob(text)
  130. trans = blob.translate(to='ru')
  131. return trans
  132. def remove_unnecessary_parts(text):
  133. """ Удаляем части текста, переводить которые не нужно.
  134. Возвращаем готовый текст для перевода """
  135. cleared_text = re.sub(r'<pre.+?>.+?</pre>|<hr/>|<a.+?>.+?</a>|<code.*?>.*?</code>|<blockquote.*?>.*?</blockquote>', 'Ё', str(text), flags=re.S)
  136. return cleared_text
  137. def insert_code_in_text(text):
  138. """ Вставляем код в переведенный текст """
  139. done_text = []
  140. count = 0
  141. text = text.split('Ё')
  142. for part in text:
  143. if count == len(cut_out_code):
  144. if len(text) != len(cut_out_code):
  145. done_text.append(text[-1])
  146. result = ''.join(done_text)
  147. return result
  148. done_text.append(part + cut_out_code[count])
  149. count += 1
  150. text = cut_out_excess(text) # Очищенный текст
  151. cut_out_code = cut_code_from_question(text) # Блоки кода из вопроса
  152. cleared_text = remove_unnecessary_parts(text) # Получаем 'чистый' текст для перевода
  153. translated_text = get_translated_text(cleared_text) # Получаем переведенный текст
  154. done = insert_code_in_text(translated_text) # Готовый вопрос
  155. return done
  156. def get_content_question(self):
  157. """ Собираем информацию со всего вопроса """
  158. def get_question_title():
  159. """ Получить заголовок вопроса """
  160. question_title = content.find('div', {'id': 'question-header'}) \
  161. .find('h1').find('a').text
  162. result = self.translate_text(question_title)
  163. return result
  164. def get_question_link():
  165. """ Получить ссылку вопроса """
  166. question_link = content.find('div', {'id': 'question-header'}) \
  167. .find('h1').find('a')['href']
  168. result = 'https://stackoverflow.com' + question_link
  169. return result
  170. def get_activity_information():
  171. """ Получить значения об активности вопроса """
  172. activity = page.find('div', {'class': 'inner-content'})
  173. # Вопрос задан
  174. question_asked = activity.find('time', {'itemprop': 'dateCreated'}).text
  175. # Последняя активность
  176. last_activity = activity.find('a', {'class': 's-link'}).text
  177. # Количество просмотров
  178. viewed = activity.find_all('span', {'class': 'fc-light'})[2] \
  179. .parent.text
  180. viewed = re.findall(r'\d+', viewed)[0]
  181. data = (question_asked, last_activity, viewed)
  182. return data
  183. def get_upvote_count():
  184. """ Получить количество голосов вопроса """
  185. upvote_count = full_question.find('div', {'itemprop': 'upvoteCount'}).text
  186. return upvote_count
  187. def get_question_content():
  188. """ Получить основное содержание вопроса """
  189. content = full_question.find('div', {'class': 'postcell'}) \
  190. .find('div', {'class': 'js-post-body'})
  191. translated_text = self.translate_text(content) # Переведенное содержание вопроса
  192. return translated_text
  193. def get_question_tags():
  194. """ Получить все теги вопроса """
  195. def get_description_tags(tag):
  196. """ Получить описание тега """
  197. link = f'https://stackoverflow.com/questions/tagged/{tag}'
  198. self.launch_browser(link)
  199. tag_description = self.browser.find_element(
  200. By.XPATH,
  201. '//div[@id="mainbar"]//p'
  202. ).text
  203. result = self.translate_text(tag_description)
  204. return result
  205. print('Смотрим теги вопроса')
  206. done_list_tags = []
  207. tags = full_question.find_all('a', {'class': 'post-tag'})
  208. for tag in tags:
  209. tag_name = tag.text
  210. tag_desc = get_description_tags(tag_name)
  211. done_list_tags.append([tag_name, tag_desc])
  212. return done_list_tags
  213. def get_related_questions():
  214. """ Получить похожие вопросы """
  215. result = []
  216. q_related = page.find('div', {'class': 'sidebar-related'}) \
  217. .find_all('div', {'class': 'spacer'})
  218. for q in q_related:
  219. vote_related_q = q.find_all('a')[0].text.strip()
  220. text_related_q = q.find_all('a')[1].text
  221. translated_text_q = self.translate_text(text_related_q) # Переводим текст
  222. href_related_q = q.find_all('a')[1]['href']
  223. result.append((vote_related_q, translated_text_q, href_related_q))
  224. return result
  225. def get_question_comments():
  226. """ Получить комментарии к вопросу """
  227. try:
  228. # Закрываем все возможные банеры
  229. #self.launch_browser(url='https://stackoverflow.com/questions/17778372/why-does-my-recursive-function-return-none')
  230. self.browser.find_element(
  231. By.XPATH,
  232. '/html/body/div[5]/div/button[1]'
  233. ).click()
  234. self.browser.implicitly_wait(5)
  235. self.browser.find_element(
  236. By.XPATH,
  237. '//*[@id="openid-buttons"]/button[4]'
  238. ).click()
  239. self.browser.implicitly_wait(5)
  240. # Нажимаем кнопку показать больше
  241. btn = self.browser.find_element(
  242. By.XPATH,
  243. '//div[@class="question"]//a[@class="js-show-link comments-link "]'
  244. )
  245. btn.click()
  246. time.sleep(2.5)
  247. except Exception as ex:
  248. # Если баннеров или вопросов больше нет - ничего не делаем
  249. pass
  250. result = []
  251. comments = page.find('div', {'class': 'js-post-comments-component'}) \
  252. .find('ul', {'class': 'comments-list'}) \
  253. .find_all('li')
  254. for comment in comments:
  255. c_actions = comment.find('div', {'class': 'comment-actions'}).text.strip()
  256. c_text = comment.find('div', {'class': 'js-comment-text-and-form'}) \
  257. .text.strip()
  258. c_text = re.split(r'\w{3}\s\d+\s\'\d+\sat\s', c_text)[0]
  259. c_text_done = self.translate_text(c_text)
  260. result.append([c_actions, c_text_done])
  261. return result
  262. def get_question_answer():
  263. """ Получить все ответы к вопросу """
  264. def get_answer_votes(answer):
  265. """ Получить количество голосов ответа """
  266. number_votes = answer.find('div', {'class': 'votecell'}) \
  267. .find('div', {'class': 'js-vote-count'}).text.strip()
  268. return number_votes
  269. def get_text_answer(answer):
  270. """ Получить текст ответа перевести его """
  271. text = answer.find('div', {'class': 'answercell'}) \
  272. .find('div', {'class': 'js-post-body'})
  273. translated_text = self.translate_text(text) # Получаем переведенный текст
  274. return translated_text
  275. def get_content_answer(all_answers, answer_from_page):
  276. """ Получаем содержимое ответа """
  277. for i, answer in enumerate(answer_from_page, 1):
  278. print(f'Ответ №{i}')
  279. number_votes = get_answer_votes(answer)
  280. text_answer = get_text_answer(answer)
  281. all_answers.append([number_votes, text_answer])
  282. def get_number_pages_with_answers():
  283. """ Проверяем есть страницы с ответами еще """
  284. result = []
  285. try:
  286. pagination = self.browser.find_elements(
  287. By.XPATH,
  288. '//div[@class="s-pagination pager-answers"][1]/*'
  289. )
  290. except: # Значит страниц, с дополнительными ответами нет
  291. return 0
  292. number_elements = len(pagination) - 1
  293. for n in range(1, number_elements + 1):
  294. link = f'https://stackoverflow.com/questions/11/calculate-relative-time-in-c-sharp?page={n}&tab=votes#tab-top'
  295. result.append(link)
  296. return result[1:]
  297. print('Смотрим ответы к вопросу')
  298. all_answers = []
  299. page = self.convert_to_bs()
  300. answer_from_page = page.find('div', {'id': 'answers'}) \
  301. .find_all('div', {'class': 'answer'}) # Все ответы с вопроса
  302. if answer_from_page == 0:
  303. return 0
  304. get_content_answer(all_answers, answer_from_page) # Достаем все из вопроса
  305. additional_p = get_number_pages_with_answers() # Узнаем, есть ли еще страницы с вопросами
  306. if additional_p: # Если есть дополнительные страницы с ответами
  307. for p in additional_p:
  308. self.launch_browser(p)
  309. page = self.convert_to_bs()
  310. answer_from_page = page.find('div', {'id': 'answers'}) \
  311. .find_all('div', {'class': 'answer'}) # Все ответы с вопроса
  312. get_content_answer(all_answers, answer_from_page) # Достаем все из вопроса
  313. return all_answers
  314. # НИЖЕ ВЫЗОВ ВСЕХ ФУНКЦИЙ
  315. page = self.convert_to_bs()
  316. content = page.find('div', {'id': 'content'})
  317. full_question = content.find('div', {'id': 'mainbar'}) \
  318. .find('div', {'class': 'question'})
  319. q_answer = get_question_answer()
  320. print(q_answer)
  321. print(type(q_answer))
  322. print(len(q_answer))
  323. for item in q_answer:
  324. print(f'Голосов {item[0]}\n{item[1]}')
  325. if not q_answer: # Если нет ответов, то и вопрос этот не нужен
  326. return
  327. self.browser.implicitly_wait(5)
  328. q_title = get_question_title()
  329. q_link = get_question_link()
  330. q_activity = get_activity_information()
  331. q_upvote_count = get_upvote_count()
  332. #q_content = get_question_content()
  333. #q_tags = get_question_tags()
  334. q_related = get_related_questions()
  335. q_comments = get_question_comments()
  336. db = DataBase(
  337. title=q_title,
  338. link=q_link,
  339. upvote=q_upvote_count,
  340. related_questions=q_related,
  341. answers=q_answer
  342. )
  343. a = Parser()