Найти тему

Web Scraping: преобразовать табличную структуру в иерархическую

Оглавление

Задача

Имеется файл (перепись 1917 года) такой структуры

248963_Нагорнов Герасим Терентьевич_Пензенский уезд_Бекетовская волость_село Михайловка_ф.158 оп.3 д.4287
248964_Нагорнов Петр Терентьевич_Пензенский уезд_Бекетовская волость_село Телегино_ф.158 оп.3 д.4287
248965_Нагорнова Александра Федоровна_Пензенский уезд_Бекетовская волость_село Михайловка_ф.158 оп.3 д.4287
248966_Новичков Лаврентий Иванович_Пензенский уезд_Дертевская волость_село Сабуровка_ф.158 оп.3 д.4287
248967_Новичков Никита Иванович_Пензенский уезд_Дертевская волость_село Сабуровка_ф.158 оп.3 д.4287

Его особенность - дублирование информации в соседних строках. В каждой из них - 6 колонок, но информация в некоторых часто повторяется. Для табличной структуры это неизбежно, если мы для каждой персоны хотим указать ее адрес: уезд-волость-село. Иерархическая структура позволит нам сохранить полноту и устранить дублирование информации. Такая структура могла бы выглядеть, например, так:

ф.158 оп.3 д.4287
Пензенский уезд
Бекетовская волость
село Михайловка
248963_Нагорнов Герасим Терентьевич
село Телегино
248964_Нагорнов Петр Терентьевич
село Михайловка
248965_Нагорнова Александра Федоровна
Дертевская волость
село Сабуровка
248966_Новичков Лаврентий Иванович
248967_Новичков Никита Иванович

В этом варианте дублирование почти ликвидировано. Дважды упоминается село Михайловка, но это уже не поправить, если не разрешается менять порядок строк. Итак, имеется табличная структура, в которой все строки имеют одинаковое число колонок, имеющих во всех строках одинаковый смысл. Порядок строк менять нельзя. Нужно преобразовать эту структуру в иерархическую, в которой повторяющиеся значения заданных колонок выносятся в заголовок и указываются один раз.

Программа на Python

dir  =  'f:/'
f  =  open ( dir + "Таблица.txt" ,  "r" ,  encoding = 'utf-8-sig' ) # открыть таблицу
nums = f.read().splitlines() # прочитать построчно
rows = [s.split('_') for s in nums] # создать список строк с разделенными колонками
g  =  open ( dir + 'Структура.xml' ,  'w' ,  encoding = 'utf-8' )
g . write ( '<?xml version="1.0"?>' )
g . write ( ' \n <root>' )
v0  =  None ;  v1 = None ;  v2 = None ;  v3 = None # значения колонок
n  = [ 'n' , 'fio' , 'u' , 'v' , 's' ,  'f' ]  # имена колонок
i0  =  2 ;  i1  =  5 ;  i2  =  3 ;  i3  =  4 # номера колонок
k = 0
line  =  f . readline ()
while line :
k += 1
line  =  line . rstrip ( ' \n ' )
row  =  line . split ( '_' )
if not k % 20000 :
print ( k , row )
cv0  =  row [ i0 ];  cv1  =  row [ i1 ];  cv2  =  row [ i2 ];  cv3  =  row [ i3 ] ;
n0  =  n [ i0 ];  n1  =  n [ i1 ];  n2  =  n [ i2 ];  n3  =  n [ i3 ] ;
if cv0 != v0 : # сменился уровень 0
if v0 != None :
g . write ( ' \n     </' + n3 + '>' + ' \n    </' + n2 + '>' + ' \n   </' + n1 + '>' + ' \n  </' + n0 + '>' )
v0 = cv0 ;  v1 = cv1 ;  v2 = cv2 ;  v3 = cv3
g . write ( ' \n  <' + n0 + '>' + cv0 + ' \n   <' + n1 + '>' + cv1 + ' \n    <' + n2 + '>' + cv2 + ' \n     <' + n3 + '>' + cv3 )
elif cv1 != v1 :
# сменился уровень 1
if v1 != None :
g . write ( ' \n     </' + n3 + '>' + ' \n    </' + n2 + '>' + ' \n   </' + n1 + '>' )
v1 = cv1 ;  v2 = cv2 ;  v3 = cv3
g . write ( ' \n   <' + n1 + '>' + cv1 + ' \n    <' + n2 + '>' + cv2 + ' \n     <' + n3 + '>' + cv3 )
elif cv2 != v2 :
# сменился уровень 2
if v2 != None :
g . write ( ' \n     </' + n3 + '>' + ' \n    </' + n2 + '>' )
v2 = cv2 ;  v3 = cv3
g . write ( ' \n    <' + n2 + '>' + cv2 + ' \n     <' + n3 + '>' + cv3 )
elif cv3 != v3 :
# сменился уровень 3
if v3 != None :
g . write ( ' \n     </' + n3 + '>' )
v3 = cv3 ;
g . write ( ' \n     <' + n3 + '>' + cv3 )
g . write ( ' \n      <p>' + row [ 1 ]+ '</p>' )
line  =  f . readline ()
f . close ()
g . write ( ' \n     </' + n3 + '>' + ' \n    </' + n2 + '>' + ' \n   </' + n1 + '>' + ' \n  </' + n0 + '>' + ' \n </root>' )
print ( 'finished' )

Программа получилась довольно большой. Разберем ее.

  1. Первый этап - открытие исходной таблицы на чтение и файла XML выходной структуры - на запись.
  2. Затем мы выбираем, какие "колонки с дублированием" следует выбрать и в каком порядке:
    v0  =  None ;  v1 = None ;  v2 = None ;  v3 = None # значения колонок
    n  = [ 'n' , 'fio' , 'u' , 'v' , 's' ,  'f' ]  # имена колонок
    i0  =  2 ;  i1  =  5 ;  i2  =  3 ;  i3  =  4 # номера колонок
    Здесь указано, что колонок с дублированием четыре, и они упорядочены в следующую иерархию: 'u','f','v','s', т.е. фонд-уезд-волость-село. Первые две колонки 'n','fio' в число дублированных не входят.
  3. Теперь мы читаем таблицу построчно
    line  =  f . readline ()
    while line :
  4. В текущей строке мы запоминаем значения дублированных колонок и их имена
    cv0  =  row [ i0 ];  cv1  =  row [ i1 ];  cv2  =  row [ i2 ];  cv3  =  row [ i3 ] ;
    n0  =  n [ i0 ];  n1  =  n [ i1 ];  n2  =  n [ i2 ];  n3  =  n [ i3 ] ;
  5. Для каждой колонки, начиная с самой главной по иерархии, уровень 0, затем уровень 1 и т.д. (на примере уровня 0, на других уровнях - аналогично): Если значение изменилось по сравнению с предыдущей строкой,
    if cv0 != v0 : # сменился уровень 0
    то
    1) закрываем текущий и все последующие уровни иерархии (если они были открыты, т.е. если строка не первая):
    if v0 != None :
    g . write ( ' \n     </' + n3 + '>' + ' \n    </' + n2 + '>' + ' \n   </' + n1 + '>' + ' \n  </' + n0 + '>' )
    2) обновляем список предыдущих значений колонок
    v0 = cv0 ;  v1 = cv1 ;  v2 = cv2 ;  v3 = cv3
    3) открываем текущий и все последующие уровни иерархии - с новыми их значениями
    g . write ( ' \n  <' + n0 + '>' + cv0 + ' \n   <' + n1 + '>' + cv1 + ' \n    <' + n2 + '>' + cv2 + ' \n     <' + n3 + '>' + cv3 )
  6. Когда обработка дублирующихся колонок завершена, записываем в выходной файл остаток исходной строки
    g . write ( ' \n      <p>' + row [ 1 ]+ '</p>' )
    В данном случае выводится только вторая колонка ('fio') - для уменьшения объема выходного файла и в связи с тем, что первая колонка 'n' - для данного конкретного входного файла - совпадает с номером строки в нем и, таким образом, не несет дополнительной информации.
  7. Отметим, что операции чтения и записи происходят построчно - каждая очередная запись читается из таблицы, результат ее обработки записывается в выходной файл (в виде одной или нескольких строк), и затем это повторяется для следующей строки. При такой обработке размер файла не важен, т.к. при обработке мы не читаем его целиком.

Результат работы программы

<?xml version="1.0"?>
<root>
<u>Городищенский уезд
<f>ф.158 оп.3 д.2612
<v>Аришкинская волость
<s>село Аришка
<p>Абрамов Сергей Матвеевич</p>
<p>Агапов Лаврентий Степанович</p>
<p>Агапов Степан Васильевич</p>
<p>Агапов Трофим Савельевич</p>
...
<p>Шурыгин Павел Иванович</p>
<p>Шурыгин Степан Ефимович</p>
<p>Шурыгина Аксинья </p>
</s>
</v>
</f>
<f>ф.158 оп.3 д.2613
<v>Аришкинская волость
<s>село Исаевка
<p>Андреев Андрей Иванович</p>
<p>Андреев Василий Петрович</p>
<p>Андреев Зиновий Петрович</p>
...
<v>Свищевская волость
<s>село Голощапово
<p>Яшина Аграфена Николаевна</p>
</s>
</v>
</f>
</u>
</root>

Анализ

Применение программы позволило первоначальную таблицу размером 56 Мбайт превратить в XML файл размером 21 Мбайт - без потери информации, за счет устранения дублирования.

Отметим, что дублирование полностью истребить не удается. Рассмотрим, на примерах почему это происходит.

Пример 1 Неполная информация

002113_Хоханова Агафья Ивановна_Городищенский уезд_Базарно-Кеньшинская волость_село Ивановские выселки_ф.158 оп.3 д.2623
002114_Хоханова Ефросинья Степановна_Городищенский уезд_Базарно-Кеньшинская волость_село Ивановские выселки_ф.158 оп.3 д.2623
002115__уезд_волость_село_ф.158 оп.3 д.2624
002116_Алябин Аким Яковлевич_Городищенский уезд_Базарно-Кеньшинская волость_село Карамалы_ф.158 оп.3 д.2624
002117_Алябин Кузьма Акимович_Городищенский уезд_Базарно-Кеньшинская волость_село Карамалы_ф.158 оп.3 д.2624

Здесь мы видим пять строк, в третьей из которых не указаны ни фамилия, ни составляющие адреса. Есть только сведения о фонде-описи-деле. Если бы этой третьей строки не было (или если бы в исходном файле значения были заполнены, и тогда, скорее всего, село в этой третьей записи было бы либо 'Ивановские выселки', либо 'Карамалы'), то не было бы смены уезда и волости в диапазоне этих пяти строк. Но в данной ситуации результат получается таким:

<p>Хоханова Агафья Ивановна</p>
<p>Хоханова Ефросинья Степановна</p>
</s>
</v>
</f>
</u>
<u>уезд
<f>ф.158 оп.3 д.2624
<v>волость
<s>село
<p></p>
</s>
</v>
</f>
</u>
<u>Городищенский уезд
<f>ф.158 оп.3 д.2624
<v>Базарно-Кеньшинская волость
<s>село Карамалы
<p>Алябин Аким Яковлевич</p>
<p>Алябин Кузьма Акимович</p>

Программа вынуждена закрыть уровни 1-4, затем открыть их снова (чтобы отобразить третью строку, которая на самом деле пуста, как мы видим), а потом опять открыть уровни 1-4. При последнем открытии программа дублирует уезд, фонд и волость, хотя по сравнению со строками 1-2, они в строках 4-5 не изменились (и только село по факту изменилось). Это проблема не программы, а вопрос полноты данных исходного файла.

Пример 2 Порядок строк

001100_Казаков Алексей Иванович_Городищенский уезд_Базарно-Кеньшинская волость_село Серман_ф.158 оп.3 д.2621
001101_Казаков Иван Михайлович_Городищенский уезд_Базарно-Кеньшинская волость_село Серман_ф.158 оп.3 д.2621
001102_Казакова Екатерина Николаевна_Городищенский уезд_Базарно-Кеньшинская волость_село Базарная Кеньша_ф.158 оп.3 д.2621
001103_Карпунин Андрей Степанович_Городищенский уезд_Базарно-Кеньшинская волость_село Серман_ф.158 оп.3 д.2621
001104_Карпунин Трофим Георгиевич_Городищенский уезд_Базарно-Кеньшинская волость_село Серман_ф.158 оп.3 д.2621

Здесь снова пять строк. Полнота данных обеспечена, но здесь другая беда. Обычно в списке сведения упорядочены по уезду, волости и селу, а внутри села - по фамилии. Но в данном случае для Екатерины Николаевны указано село "Базарная Кеньша", которое в этом диапазоне окружено селом "Серман". Результат программы для этого диапазона строк имеет вид:

<p>Казаков Алексей Иванович</p>
<p>Казаков Иван Михайлович</p>
</s>
<s>село Базарная Кеньша
<p>Казакова Екатерина Николаевна</p>
</s>
<s>село Серман
<p>Карпунин Андрей Степанович</p>
<p>Карпунин Трофим Георгиевич</p>

Это также не вина программы. Поскольку мы договорились, что порядок строк (без разбора ситуации с привлечением оригинала исходного документа) не меняем, то устранить повторение села Сарман (которое было сначала указано для Казакова, а потом повторено перед Карпуниным) нельзя.

Пример 3 Ошибка (неоднозначность) в оригинале

097418_Воронков Павел Иванович_Керенский уезд_Шеинская волость_село Алексеевка_ф.158 оп.3 д.3223
097419_Воронков Петр Андреевич_Керенский уезд_Шеинская волость_село Алекс._ф.158 оп.3 д.3223
097420_Воронков Сергей Степанович_Керенский уезд_Шеинская волость_село Алексеевка_ф.158 оп.3 д.3223

В этом примере намного меньше строк, чем в предыдущих (только три, а в тех было аж целых пять). Но это мало помогает разрешить дилемму. Это ж как, извиняюсь, понимать, "село Алекс." - это и есть "село Алексеевка", али как? Без первоисточника (или на худой конец, поллитры) не разберешь. Поэтому то,что программа выдает в таких случаях, возможно, вас разочарует:

<p>Воронков Павел Иванович</p>
</s>
<s>село Алекс.
<p>Воронков Петр Андреевич</p>
</s>
<s>село Алексеевка
<p>Воронков Сергей Степанович</p>

Не стреляйте в программу, она исполняет ваши желания (кроме тайных), как может..

Подписаться на канал Математика и программирование
Вокруг ЕГЭ: разложить x^5+1 на множители с вещественными коэффициентами
Web Scraping: преобразовать иерархическую структуру в табличную
Web Scraping: преобразовать табличную структуру в иерархическую
Web Scraping: всероссийская перепись 1917
Как я поженил Лагранжа и сигмоиду
Интерполяция функций и правило Лопиталя

Подписаться на канал Новости из царской России
Вокруг ЕГЭ: разложить x^5+1 на множители с вещественными коэффициентами
Web Scraping: преобразовать иерархическую структуру в табличную
Web Scraping: преобразовать табличную структуру в иерархическую
Web Scraping: всероссийская перепись 1917
Как я поженил Лагранжа и сигмоиду

Оглавление статей канала "Новости из царской России"

YouTube "Новости из царской России"

Обсудить в групповом чате

News from ancient Russia

Персональная история русскоязычного мира