Что парсим
Из органических результатов:Примечание: На Google Scholar есть максимальный лимит в 100 страниц, поэтому когда вы видите About xxx.xxx results это не означает что все результаты отображаются и их можно спарсить. Так же как это происходит с Гугл поиском.
Из цитируемых результатов:
Что понадобится
Отдельное вирутуальное окружениеЕсли вы не работали с виртуальным окружением ранее, взгляните на посвященный этому блог пост на английском языке - Python virtual environments tutorial using Virtualenv and Poetry.
Вкратце, это штука, которая создает независимый набор установленных библиотек включая возможность установки разных версий Python которые могут сосуществовать друг с другом одновременно на одной системе, что в свою очередь предотвращает конфликты библиотек и разных версий Python.
Примечание: использование виртуальной среды не является строгим требованием.
Установка библиотек:
pip install google-search-results
pip install pandas
Процесс
Если объяснение не нужно:
- забирайте код в секции весь код целиком,
- забирайте код целиком из GitHub репозитория,
- пробуйте сразу в online IDE.
Органические результаты
import osfrom serpapi import GoogleSearch
from urllib.parse import urlsplit, parse_qsl
def organic_results():
print("extracting organic results..")
params = {
"api_key": os.getenv("API_KEY"), # ключ для аутентификации в SerpApi
"engine": "google_scholar",
"q": "minecraft redstone system structure characteristics strength", # поисковый запрос
"hl": "en", # язык
"as_ylo": "2017", # с 2017
"as_yhi": "2021", # до 2021
"start": "0" # первая страница
}
search = GoogleSearch(params)
organic_results_data = []
loop_is_true = True
while loop_is_true:
results = search.get_dict()
print(f"Currently extracting page №{results['serpapi_pagination']['current']}..")
for result in results["organic_results"]:
position = result["position"]
title = result["title"]
publication_info_summary = result["publication_info"]["summary"]
result_id = result["result_id"]
link = result.get("link")
result_type = result.get("type")
snippet = result.get("snippet")
try:
file_title = result["resources"][0]["title"]
except: file_title = None
try:
file_link = result["resources"][0]["link"]
except: file_link = None
try:
file_format = result["resources"][0]["file_format"]
except: file_format = None
try:
cited_by_count = int(result["inline_links"]["cited_by"]["total"])
except: cited_by_count = None
cited_by_id = result.get("inline_links", {}).get("cited_by", {}).get("cites_id", {})
cited_by_link = result.get("inline_links", {}).get("cited_by", {}).get("link", {})
try:
total_versions = int(result["inline_links"]["versions"]["total"])
except: total_versions = None
all_versions_link = result.get("inline_links", {}).get("versions", {}).get("link", {})
all_versions_id = result.get("inline_links", {}).get("versions", {}).get("cluster_id", {})
organic_results_data.append({
"page_number": results["serpapi_pagination"]["current"],
"position": position + 1,
"result_type": result_type,
"title": title,
"link": link,
"result_id": result_id,
"publication_info_summary": publication_info_summary,
"snippet": snippet,
"cited_by_count": cited_by_count,
"cited_by_link": cited_by_link,
"cited_by_id": cited_by_id,
"total_versions": total_versions,
"all_versions_link": all_versions_link,
"all_versions_id": all_versions_id,
"file_format": file_format,
"file_title": file_title,
"file_link": file_link,
})
if "next" in results["serpapi_pagination"]:
search.params_dict.update(dict(parse_qsl(urlsplit(results["serpapi_pagination"]["next"]).query)))
else:
loop_is_true = False
return organic_results_data
Объяснение парсинга органических результатов используя пагинацию
Импортируем os, serpapi, urllib библиотеки:import os
from serpapi import GoogleSearch
from urllib.parse import urlsplit, parse_qsl
Создаем и передаем поисковые параметры в GoogleSearch() где происходит все извлечение данных на бэкенде SerpApi:
params = {
"api_key": os.getenv("API_KEY"), # ключ для аутентификации в SerpApi
"engine": "google_scholar",
"q": "minecraft redstone system structure characteristics strength", # поисковый запрос
"hl": "en", # язык
"as_ylo": "2017", # с 2017
"as_yhi": "2021", # до 2021
"start": "0" # первая страница
}
search = GoogleSearch(params) # извлечение данных происходит тут
Создаём временный список list() для того чтобы сохранить данные которые будут дальше использоваться для сохранения в CSV или переданыcite_results() функции:
organic_results_data = []
Создаём while цикл для парсинга данных со всех доступных страниц:
loop_is_true = True
while loop_is_true:
results = search.get_dict()
# парсинг данных происходит тутачки..
if "next" in results["serpapi_pagination"]:
search.params_dict.update(dict(parse_qsl(urlsplit(results["serpapi_pagination"]["next"]).query)))
else:
loop_is_true = False
- Если нет ссылки на следующую "next" страницу - while прекратится установив loop_is_true на False.
- Если есть ссылка на следующую "next" страницу, search.params_dict.update разберет ссылку на части и передаст её к GoogleSearch(params) для результатов с новой страницы.
for result in results["organic_results"]:
position = result["position"]
title = result["title"]
publication_info_summary = result["publication_info"]["summary"]
result_id = result["result_id"]
link = result.get("link")
result_type = result.get("type")
snippet = result.get("snippet")
try:
file_title = result["resources"][0]["title"]
except: file_title = None
try:
file_link = result["resources"][0]["link"]
except: file_link = None
try:
file_format = result["resources"][0]["file_format"]
except: file_format = None
try:
cited_by_count = int(result["inline_links"]["cited_by"]["total"])
except: cited_by_count = None
cited_by_id = result.get("inline_links", {}).get("cited_by", {}).get("cites_id", {})
cited_by_link = result.get("inline_links", {}).get("cited_by", {}).get("link", {})
try:
total_versions = int(result["inline_links"]["versions"]["total"])
except: total_versions = None
all_versions_link = result.get("inline_links", {}).get("versions", {}).get("link", {})
all_versions_id = result.get("inline_links", {}).get("versions", {}).get("cluster_id", {})
- try/except блоки были использованы для обработки None значений когда они отсутствуют из Google бэкенда.
Добавляем извлеченные данные во временный list() список:
organic_results_data = []
# тут парсинг и while цикл...
organic_results_data.append({
"page_number": results["serpapi_pagination"]["current"],
"position": position + 1,
"result_type": result_type,
"title": title,
"link": link,
"result_id": result_id,
"publication_info_summary": publication_info_summary,
"snippet": snippet,
"cited_by_count": cited_by_count,
"cited_by_link": cited_by_link,
"cited_by_id": cited_by_id,
"total_versions": total_versions,
"all_versions_link": all_versions_link,
"all_versions_id": all_versions_id,
"file_format": file_format,
"file_title": file_title,
"file_link": file_link,
})
Возвращаем временный list() список с данными которые будут использоваться позже при парсинге Цитируемых результатов:
return organic_results_data
Цитируемые результаты
В этой секции мы используем возвращенные данные из органической выдачи и передадим result_id в поисковый запрос для того чтобы спарсить цитируемые результаты.Если у вас уже есть result_ids, вы можете пропустить парсинг Органических результатов:
# если у вас есть список result_ids
result_ids = ["FDc6HiktlqEJ"..."FDc6Hikt21J"]
for citation in result_ids:
params = {
"api_key": "API_KEY", # ключ для аутентификации в SerpApi
"engine": "google_scholar_cite", # движок для парсинга цитируемых результатов
"q": citation # FDc6HiktlqEJ ... FDc6Hikt21J
}
search = GoogleSearch(params)
results = search.get_dict()
# дальнейший код парсинга..
Ниже примера кода парсинга цитируемых результатов вы так же найдете пошаговое объяснение того что в нём происходит.
import os
from serpapi import GoogleSearch
from google_scholar_organic_results import organic_results
def cite_results():
print("extracting cite results..")
citation_results = []
for citation in organic_results():
params = {
"api_key": os.getenv("API_KEY"), # ключ для аутентификации в SerpApi
"engine": "google_scholar_cite", # # движок для парсинга цитируемых результатов
"q": citation["result_id"] # # FDc6HiktlqEJ ... FDc6Hikt21J
}
search = GoogleSearch(params)
results = search.get_dict()
print(f"Currently extracting {citation['result_id']} citation ID.")
for result in results["citations"]:
cite_title = result["title"]
cite_snippet = result["snippet"]
citation_results.append({
"organic_result_title": citation["title"],
"organic_result_link": citation["link"],
"citation_title": cite_title,
"citation_snippet": cite_snippet
})
return citation_results
Объяснение парсинга цитируемых результатов
Создаём временный список list() для хранения извлеченных данных:citation_results = []
Создаём for цикл для итерации по organic_results() результатам и передаем result_id в "q" поисковый запрос:
for citation in organic_results():
params = {
"api_key": os.getenv("API_KEY"), # ключ для аутентификации в SerpApi
"engine": "google_scholar_cite", # # движок для парсинга цитируемых результатов
"q": citation["result_id"] # # FDc6HiktlqEJ ... FDc6Hikt21J
}
search = GoogleSearch(params) # парсинг на бэкенде SerpApi
results = search.get_dict() # JSON конвертируется в словарь
Создаём второй for цикл и достукиваемся к данный таким же способом как и до словаря:
for result in results["citations"]:
cite_title = result["title"]
cite_snippet = result["snippet"]
Добавляем извелченные данные во временный список list() как словарь:
citation_results.append({
"organic_result_title": citation["title"], # чтобы понимать откуда берутся Цитируемые результаты
"organic_result_link": citation["link"], # чтобы понимать откуда берутся Цитируемые результаты
"citation_title": cite_title,
"citation_snippet": cite_snippet
})
Возвращаем временный список list():
return citation_results
Сохраняем в CSV
Нам только нужно передать возвращенные данные из органических и цитируемых результатов в DataFrame data аргумент и сохранить to_csv().import pandas as pd
from google_scholar_organic_results import organic_results
from google_scholar_cite_results import cite_results
print("waiting for organic results to save..")
pd.DataFrame(data=organic_results())
.to_csv("google_scholar_organic_results.csv", encoding="utf-8", index=False)
print("waiting for cite results to save..")
pd.DataFrame(data=cite_results())
.to_csv("google_scholar_citation_results.csv", encoding="utf-8", index=False)
Объяснение процесса сохранения в CSV
Импортируем organic_results() и cite_results() откуда возвращаются данные, и библиотеку pandas:import pandas as pd
from google_scholar_organic_results import organic_results
from google_scholar_cite_results import cite_results
Сохраняем органические результаты to_csv():
pd.DataFrame(data=organic_results()) \
.to_csv("google_scholar_organic_results.csv", encoding="utf-8", index=False)
Сохраняем цитируемые результаты to_csv():
pd.DataFrame(data=cite_results()) \
.to_csv("google_scholar_citation_results.csv", encoding="utf-8", index=False)
- data аргумент внутри DataFrame это извлеченные данные.
- encoding='utf-8' аргумент просто для того чтобы все было корректно сохранено. Я использовал этот аргумент явно, несмотря на то что это его дефолтное значение.
- index=False аргумент чтобы убрать дефолтные номера строк pandas.
Сохраняем в SQLite
После этой секции вы узнаете о том как:- сохранять данные в SQLite используя pandas,
- функционирует SQLite,
- подключаться и разрывать соединение с SQLite,
- создавать и удалять таблицы/колонки,
- добавлять данные в for цикле.
1. открывается соединение
2. транзакция начинается
3. выполняется инструкция
4. транзакция завершается
5. закрывается соединение
Сохранение данные в SQLite используя pandas
# https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.to_sql.htmlimport sqlite3
import pandas as pd
from google_scholar_organic_results import organic_results
from google_scholar_cite_results import cite_results
conn = sqlite3.connect("google_scholar_results.db")
# создавать таблицу ручными SQL запросами не нужно,
# pandas это сделает автоматически.
# сохраняет органические результаты в SQLite
pd.DataFrame(organic_results()).to_sql(name="google_scholar_organic_results",
con=conn,
if_exists="append",
index=False)
# сохраняет цитируемые результаты в SQLite
pd.DataFrame(cite_results()).to_sql(name="google_scholar_cite_results",
con=conn,
if_exists="append",
index=False)
conn.commit()
conn.close()
- name это название SQL таблицы.
- con это соединение с базой данных.
- if_exists скажет pandas как себя вести если таблица уже существует. По умолчанию оно не сработает "fail" и вызовет raise ошибку ValueError. В данном случае pandas будет добавлять данные.
- index=False чтобы убрать индекс столбцов от DataFrame.
Другой способ сохранения данных вручную используя запросы SQLite
import sqlite3conn = sqlite3.connect("google_scholar_results.db")
conn.execute("""CREATE TABLE google_scholar_organic_results (
page_number integer,
position integer,
result_type text,
title text,
link text,
snippet text,
result_id text,
publication_info_summary text,
cited_by_count integer,
cited_by_link text,
cited_by_id text,
total_versions integer,
all_versions_link text,
all_versions_id text,
file_format text,
file_title text,
file_link text)""")
conn.commit()
conn.execute("""CREATE TABLE google_scholar_cite_results (
organic_results_title text,
organic_results_link text,
citation_title text,
citation_link text)""")
conn.commit()
# сохраняем органические результаты
for item in organic_results():
conn.execute("""INSERT INTO google_scholar_organic_results
VALUES page_number,
osition,
:result_type,
:title,
:link,
:snippet,
:result_id,
ublication_info_summary,
:cited_by_count,
:cited_by_link,
:cited_by_id,
:total_versions,
:all_versions_link,
:all_versions_id,
:file_format,
:file_title,
:file_link)""",
{"page_number": item["page_number"],
"position": item["position"],
"result_type": item["type"],
"title": item["title"],
"link": item["link"],
"snippet": item["snippet"],
"result_id": item["result_id"],
"publication_info_summary": item["publication_info_summary"],
"cited_by_count": item["cited_by_count"],
"cited_by_link": item["cited_by_link"],
"cited_by_id": item["cited_by_id"],
"total_versions": item["total_versions"],
"all_versions_link": item["all_versions_link"],
"all_versions_id": item["all_versions_id"],
"file_format": item["file_format"],
"file_title": item["file_title"],
"file_link": item["file_link"]})
conn.commit()
# сохраняем цитируемые результаты
for cite_result in cite_results():
conn.execute("""INSERT INTO google_scholar_cite_results
VALUES organic_result_title,
rganic_result_link,
:citation_title,
:citation_snippet)""",
{"organic_result_title": cite_result["organic_result_title"],
"organic_result_link": cite_result["organic_result_link"],
"citation_title": cite_result["citation_title"],
"citation_snippet": cite_result["citation_snippet"]})
conn.commit()
conn.close() # явно лучше, чем неявно
Объяснение сохранения извлеченных данных вручную прописывая запросы SQLite
Импортируем sqlite3 библиотеку:import sqlite3
Соединяемся с существующей базе данных или даём новое имя и библиотека создаст базу данных:
conn = sqlite3.connect("google_scholar_results.db")
Создаём таблицу органические результаты, указываем тип данных и применяем изменения:
conn.execute("""CREATE TABLE google_scholar_organic_results (
page_number integer,
position integer,
result_type text,
title text,
link text,
snippet text,
result_id text,
publication_info_summary text,
cited_by_count integer,
cited_by_link text,
cited_by_id text,
total_versions integer,
all_versions_link text,
all_versions_id text,
file_format text,
file_title text,
file_link text)""")
conn.commit()
Создаём таблицу цитируемые результаты, указываем тип данных и применяем изменения:
conn.execute("""CREATE TABLE google_scholar_cite_results (
organic_results_title text,
organic_results_link text,
citation_title text,
citation_link text)""")
conn.commit()
Добавляем извлеченные органические результаты в таблицу используя for цикл:
for item in organic_results():
conn.execute("""INSERT INTO google_scholar_organic_results
VALUES page_number,
osition,
:result_type,
:title,
:link,
:snippet,
:result_id,
ublication_info_summary,
:cited_by_count,
:cited_by_link,
:cited_by_id,
:total_versions,
:all_versions_link,
:all_versions_id,
:file_format,
:file_title,
:file_link)""",
{"page_number": item["page_number"],
"position": item["position"],
"result_type": item["type"],
"title": item["title"],
"link": item["link"],
"snippet": item["snippet"],
"result_id": item["result_id"],
"publication_info_summary": item["publication_info_summary"],
"cited_by_count": item["cited_by_count"],
"cited_by_link": item["cited_by_link"],
"cited_by_id": item["cited_by_id"],
"total_versions": item["total_versions"],
"all_versions_link": item["all_versions_link"],
"all_versions_id": item["all_versions_id"],
"file_format": item["file_format"],
"file_title": item["file_title"],
"file_link": item["file_link"]})
conn.commit()
Добавляем извлеченные цитируемые результаты в таблицу используя for цикл:
for cite_result in cite_results():
conn.execute("""INSERT INTO google_scholar_cite_results
VALUES organic_result_title,
rganic_result_link,
:citation_title,
:citation_snippet)""",
{"organic_result_title": cite_result["organic_result_title"],
"organic_result_link": cite_result["organic_result_link"],
"citation_title": cite_result["citation_title"],
"citation_snippet": cite_result["citation_snippet"]})
conn.commit()
Закрываем cоединение с базой данных:
conn.close()
Дополнительные полезные команды:
# удалить все данные из таблицы
conn.execute("DELETE FROM google_scholar_organic_results")
# удалить таблицу
conn.execute("DROP TABLE google_scholar_organic_results")
# удалить колонку
conn.execute("ALTER TABLE google_scholar_organic_results DROP COLUMN authors")
# добавить колнку
conn.execute("ALTER TABLE google_scholar_organic_results ADD COLUMN snippet text")
Код целиком - парсинг
import osfrom serpapi import GoogleSearch
from urllib.parse import urlsplit, parse_qsl
def organic_results():
print("extracting organic results..")
params = {
"api_key": os.getenv("API_KEY"),
"engine": "google_scholar",
"q": "minecraft redstone system structure characteristics strength", # поисковый запрос
"hl": "en", # язык
"as_ylo": "2017", # от 2017
"as_yhi": "2021", # до 2021
"start": "0"
}
search = GoogleSearch(params)
organic_results_data = []
loop_is_true = True
while loop_is_true:
results = search.get_dict()
print(f"Currently extracting page №{results['serpapi_pagination']['current']}..")
for result in results["organic_results"]:
position = result["position"]
title = result["title"]
publication_info_summary = result["publication_info"]["summary"]
result_id = result["result_id"]
link = result.get("link")
result_type = result.get("type")
snippet = result.get("snippet")
try:
file_title = result["resources"][0]["title"]
except: file_title = None
try:
file_link = result["resources"][0]["link"]
except: file_link = None
try:
file_format = result["resources"][0]["file_format"]
except: file_format = None
try:
cited_by_count = int(result["inline_links"]["cited_by"]["total"])
except: cited_by_count = None
cited_by_id = result.get("inline_links", {}).get("cited_by", {}).get("cites_id", {})
cited_by_link = result.get("inline_links", {}).get("cited_by", {}).get("link", {})
try:
total_versions = int(result["inline_links"]["versions"]["total"])
except: total_versions = None
all_versions_link = result.get("inline_links", {}).get("versions", {}).get("link", {})
all_versions_id = result.get("inline_links", {}).get("versions", {}).get("cluster_id", {})
organic_results_data.append({
"page_number": results["serpapi_pagination"]["current"],
"position": position + 1,
"result_type": result_type,
"title": title,
"link": link,
"result_id": result_id,
"publication_info_summary": publication_info_summary,
"snippet": snippet,
"cited_by_count": cited_by_count,
"cited_by_link": cited_by_link,
"cited_by_id": cited_by_id,
"total_versions": total_versions,
"all_versions_link": all_versions_link,
"all_versions_id": all_versions_id,
"file_format": file_format,
"file_title": file_title,
"file_link": file_link,
})
if "next" in results["serpapi_pagination"]:
search.params_dict.update(dict(parse_qsl(urlsplit(results["serpapi_pagination"]["next"]).query)))
else:
loop_is_true = False
return organic_results_data
def cite_results():
print("extracting cite results..")
citation_results = []
for citation in organic_results():
params = {
"api_key": os.getenv("API_KEY"),
"engine": "google_scholar_cite",
"q": citation["result_id"]
}
search = GoogleSearch(params)
results = search.get_dict()
print(f"Currently extracting {citation['result_id']} citation ID.")
for result in results["citations"]:
cite_title = result["title"]
cite_snippet = result["snippet"]
citation_results.append({
"organic_result_title": citation["title"],
"organic_result_link": citation["link"],
"citation_title": cite_title,
"citation_snippet": cite_snippet
})
return citation_results
# пример вывода при парсинге органических результатов и сохранение в SQL:
'''
extracting organic results..
Currently extracting page №1..
Currently extracting page №2..
Currently extracting page №3..
Currently extracting page №4..
Currently extracting page №5..
Currently extracting page №6..
Done extracting organic results.
Saved to SQL Lite database.
'''
Код целиком - сохранение
import pandas as pdimport sqlite3
from google_scholar_organic_results import organic_results
from google_scholar_cite_results import cite_results
# Один из способов сохранить данные в БД используя Pandas
print("waiting for organic results to save..")
organic_df = pd.DataFrame(data=organic_results())
organic_df.to_csv("google_scholar_organic_results.csv", encoding="utf-8", index=False)
print("waiting for cite results to save..")
cite_df = pd.DataFrame(data=cite_results())
cite_df.to_csv("google_scholar_citation_results.csv", encoding="utf-8", index=False)
# ------------------------------
# Другой способ сохранить данные в БФ в ручную прописывая SQLite запросы
conn = sqlite3.connect("google_scholar_results.db")
conn.execute("""CREATE TABLE google_scholar_organic_results (
page_number integer,
position integer,
result_type text,
title text,
link text,
snippet text,
result_id text,
publication_info_summary text,
cited_by_count integer,
cited_by_link text,
cited_by_id text,
total_versions integer,
all_versions_link text,
all_versions_id text,
file_format text,
file_title text,
file_link text)""")
conn.commit()
conn.execute("""CREATE TABLE google_scholar_cite_results (
organic_results_title text,
organic_results_link text,
citation_title text,
citation_link text)""")
conn.commit()
for item in organic_results():
conn.execute("""INSERT INTO google_scholar_organic_results
VALUES page_number,
osition,
:result_type,
:title,
:link,
:snippet,
:result_id,
ublication_info_summary,
:cited_by_count,
:cited_by_link,
:cited_by_id,
:total_versions,
:all_versions_link,
:all_versions_id,
:file_format,
:file_title,
:file_link)""",
{"page_number": item["page_number"],
"position": item["position"],
"result_type": item["type"],
"title": item["title"],
"link": item["link"],
"snippet": item["snippet"],
"result_id": item["result_id"],
"publication_info_summary": item["publication_info_summary"],
"cited_by_count": item["cited_by_count"],
"cited_by_link": item["cited_by_link"],
"cited_by_id": item["cited_by_id"],
"total_versions": item["total_versions"],
"all_versions_link": item["all_versions_link"],
"all_versions_id": item["all_versions_id"],
"file_format": item["file_format"],
"file_title": item["file_title"],
"file_link": item["file_link"]})
conn.commit()
for cite_result in cite_results():
conn.execute("""INSERT INTO google_scholar_cite_results
VALUES organic_result_title,
rganic_result_link,
:citation_title,
:citation_snippet)""",
{"organic_result_title": cite_result["organic_result_title"],
"organic_result_link": cite_result["organic_result_link"],
"citation_title": cite_result["citation_title"],
"citation_snippet": cite_result["citation_snippet"]})
conn.commit()
conn.close()
print("Saved to SQL Lite database.")
# пример вывода:
'''
extracting organic results..
Currently extracting page №1..
...
Currently extracting page №4..
extracting cite results..
extracting organic results..
Currently extracting page №1..
...
Currently extracting page №4..
Currently extracting 60l4wsP6Ps0J citation ID.
Currently extracting 9hkhIFu_BhAJ citation ID.
...
Saved to SQL Lite database.
'''
Ссылки
- GitHub репозиторий
- Код с парсингом, сохранением в CSV в онлайн IDE
- Google Scholar API
- SerpApi библиотеки
Что дальше
С этими данными должно быть возможно сделать какое-нибудь исследование, или визуализацию по определенной дисциплине. Классным дополнением к этому скрипту будет добавить возможность запускать его каждую неделю или месяц для парсинга дополнительных данных.Следующий блог пост будет о парсинге Профилей с пагинацией, а так же Авторских результатов.
Если вы хотите парсить данные без необходимости писать парсер с нуля, разбираться как обойти блокировки от поисковых систем, как увеличить объем запросов, или, как парсить данные с JavaScript - попробуйте SerpApi или свяжитесь с SerpApi.
Парсинг исторических данных с Google Scholar используя Python
? Кратко о сути : парсинг исторических органических и цитируемых результатов 2017-2021 годов с Google Scholar используя пагинацию. Следом их сохранение в CSV и SQLite БД используя Python и библиотеку...
habr.com